##// END OF EJS Templates
* Added support for prompt and history requests to the kernel manager and Qt console frontend....
epatters -
Show More
@@ -1,1279 +1,1290 b''
1 1 # Standard library imports
2 2 import re
3 3 import sys
4 4 from textwrap import dedent
5 5
6 6 # System library imports
7 7 from PyQt4 import QtCore, QtGui
8 8
9 9 # Local imports
10 10 from ansi_code_processor import QtAnsiCodeProcessor
11 11 from completion_widget import CompletionWidget
12 12
13 13
14 14 class ConsoleWidget(QtGui.QWidget):
15 15 """ An abstract base class for console-type widgets. This class has
16 16 functionality for:
17 17
18 18 * Maintaining a prompt and editing region
19 19 * Providing the traditional Unix-style console keyboard shortcuts
20 20 * Performing tab completion
21 21 * Paging text
22 22 * Handling ANSI escape codes
23 23
24 24 ConsoleWidget also provides a number of utility methods that will be
25 25 convenient to implementors of a console-style widget.
26 26 """
27 27
28 28 # Whether to process ANSI escape codes.
29 29 ansi_codes = True
30 30
31 31 # The maximum number of lines of text before truncation.
32 32 buffer_size = 500
33 33
34 34 # Whether to use a list widget or plain text output for tab completion.
35 35 gui_completion = True
36 36
37 37 # Whether to override ShortcutEvents for the keybindings defined by this
38 38 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
39 39 # priority (when it has focus) over, e.g., window-level menu shortcuts.
40 40 override_shortcuts = False
41 41
42 42 # Signals that indicate ConsoleWidget state.
43 43 copy_available = QtCore.pyqtSignal(bool)
44 44 redo_available = QtCore.pyqtSignal(bool)
45 45 undo_available = QtCore.pyqtSignal(bool)
46 46
47 47 # Signal emitted when paging is needed and the paging style has been
48 48 # specified as 'custom'.
49 49 custom_page_requested = QtCore.pyqtSignal(object)
50 50
51 51 # Protected class variables.
52 52 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
53 53 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
54 54 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
55 55 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
56 56 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
57 57 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
58 58 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
59 59 _shortcuts = set(_ctrl_down_remap.keys() +
60 60 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
61 61
62 62 #---------------------------------------------------------------------------
63 63 # 'QObject' interface
64 64 #---------------------------------------------------------------------------
65 65
66 66 def __init__(self, kind='plain', paging='inside', parent=None):
67 67 """ Create a ConsoleWidget.
68 68
69 69 Parameters
70 70 ----------
71 71 kind : str, optional [default 'plain']
72 72 The type of underlying text widget to use. Valid values are 'plain',
73 73 which specifies a QPlainTextEdit, and 'rich', which specifies a
74 74 QTextEdit.
75 75
76 76 paging : str, optional [default 'inside']
77 77 The type of paging to use. Valid values are:
78 78 'inside' : The widget pages like a traditional terminal pager.
79 79 'hsplit' : When paging is requested, the widget is split
80 80 horizontally. The top pane contains the console,
81 81 and the bottom pane contains the paged text.
82 82 'vsplit' : Similar to 'hsplit', except that a vertical splitter
83 83 used.
84 84 'custom' : No action is taken by the widget beyond emitting a
85 85 'custom_page_requested(str)' signal.
86 86 'none' : The text is written directly to the console.
87 87
88 88 parent : QWidget, optional [default None]
89 89 The parent for this widget.
90 90 """
91 91 super(ConsoleWidget, self).__init__(parent)
92 92
93 93 # Create the layout and underlying text widget.
94 94 layout = QtGui.QStackedLayout(self)
95 95 layout.setMargin(0)
96 96 self._control = self._create_control(kind)
97 97 self._page_control = None
98 98 self._splitter = None
99 99 if paging in ('hsplit', 'vsplit'):
100 100 self._splitter = QtGui.QSplitter()
101 101 if paging == 'hsplit':
102 102 self._splitter.setOrientation(QtCore.Qt.Horizontal)
103 103 else:
104 104 self._splitter.setOrientation(QtCore.Qt.Vertical)
105 105 self._splitter.addWidget(self._control)
106 106 layout.addWidget(self._splitter)
107 107 else:
108 108 layout.addWidget(self._control)
109 109
110 110 # Create the paging widget, if necessary.
111 111 self._page_style = paging
112 112 if paging in ('inside', 'hsplit', 'vsplit'):
113 113 self._page_control = self._create_page_control()
114 114 if self._splitter:
115 115 self._page_control.hide()
116 116 self._splitter.addWidget(self._page_control)
117 117 else:
118 118 layout.addWidget(self._page_control)
119 119 elif paging not in ('custom', 'none'):
120 120 raise ValueError('Paging style %s unknown.' % repr(paging))
121 121
122 122 # Initialize protected variables. Some variables contain useful state
123 123 # information for subclasses; they should be considered read-only.
124 124 self._ansi_processor = QtAnsiCodeProcessor()
125 125 self._completion_widget = CompletionWidget(self._control)
126 126 self._continuation_prompt = '> '
127 127 self._continuation_prompt_html = None
128 128 self._executing = False
129 129 self._prompt = ''
130 130 self._prompt_html = None
131 131 self._prompt_pos = 0
132 132 self._reading = False
133 133 self._reading_callback = None
134 134 self._tab_width = 8
135 135
136 136 # Set a monospaced font.
137 137 self.reset_font()
138 138
139 139 def eventFilter(self, obj, event):
140 140 """ Reimplemented to ensure a console-like behavior in the underlying
141 141 text widget.
142 142 """
143 143 # Re-map keys for all filtered widgets.
144 144 etype = event.type()
145 145 if etype == QtCore.QEvent.KeyPress and \
146 146 self._control_key_down(event.modifiers()) and \
147 147 event.key() in self._ctrl_down_remap:
148 148 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
149 149 self._ctrl_down_remap[event.key()],
150 150 QtCore.Qt.NoModifier)
151 151 QtGui.qApp.sendEvent(obj, new_event)
152 152 return True
153 153
154 154 # Override shortucts for all filtered widgets. Note that on Mac OS it is
155 155 # always unnecessary to override shortcuts, hence the check below (users
156 156 # should just use the Control key instead of the Command key).
157 157 elif etype == QtCore.QEvent.ShortcutOverride and \
158 158 sys.platform != 'darwin' and \
159 159 self._control_key_down(event.modifiers()) and \
160 160 event.key() in self._shortcuts:
161 161 event.accept()
162 162 return False
163 163
164 164 elif obj == self._control:
165 165 # Disable moving text by drag and drop.
166 166 if etype == QtCore.QEvent.DragMove:
167 167 return True
168 168
169 169 elif etype == QtCore.QEvent.KeyPress:
170 170 return self._event_filter_console_keypress(event)
171 171
172 172 elif obj == self._page_control:
173 173 if etype == QtCore.QEvent.KeyPress:
174 174 return self._event_filter_page_keypress(event)
175 175
176 176 return super(ConsoleWidget, self).eventFilter(obj, event)
177 177
178 178 #---------------------------------------------------------------------------
179 179 # 'QWidget' interface
180 180 #---------------------------------------------------------------------------
181 181
182 182 def sizeHint(self):
183 183 """ Reimplemented to suggest a size that is 80 characters wide and
184 184 25 lines high.
185 185 """
186 186 style = self.style()
187 187 opt = QtGui.QStyleOptionHeader()
188 188 font_metrics = QtGui.QFontMetrics(self.font)
189 189 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
190 190
191 191 width = font_metrics.width(' ') * 80
192 192 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
193 193 if self._page_style == 'hsplit':
194 194 width = width * 2 + splitwidth
195 195
196 196 height = font_metrics.height() * 25
197 197 if self._page_style == 'vsplit':
198 198 height = height * 2 + splitwidth
199 199
200 200 return QtCore.QSize(width, height)
201 201
202 202 #---------------------------------------------------------------------------
203 203 # 'ConsoleWidget' public interface
204 204 #---------------------------------------------------------------------------
205 205
206 206 def can_paste(self):
207 207 """ Returns whether text can be pasted from the clipboard.
208 208 """
209 209 # Accept only text that can be ASCII encoded.
210 210 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
211 211 text = QtGui.QApplication.clipboard().text()
212 212 if not text.isEmpty():
213 213 try:
214 214 str(text)
215 215 return True
216 216 except UnicodeEncodeError:
217 217 pass
218 218 return False
219 219
220 220 def clear(self, keep_input=True):
221 221 """ Clear the console, then write a new prompt. If 'keep_input' is set,
222 222 restores the old input buffer when the new prompt is written.
223 223 """
224 224 if keep_input:
225 225 input_buffer = self.input_buffer
226 226 self._control.clear()
227 227 self._show_prompt()
228 228 if keep_input:
229 229 self.input_buffer = input_buffer
230 230
231 231 def copy(self):
232 232 """ Copy the current selected text to the clipboard.
233 233 """
234 234 self._control.copy()
235 235
236 236 def execute(self, source=None, hidden=False, interactive=False):
237 237 """ Executes source or the input buffer, possibly prompting for more
238 238 input.
239 239
240 240 Parameters:
241 241 -----------
242 242 source : str, optional
243 243
244 244 The source to execute. If not specified, the input buffer will be
245 245 used. If specified and 'hidden' is False, the input buffer will be
246 246 replaced with the source before execution.
247 247
248 248 hidden : bool, optional (default False)
249 249
250 250 If set, no output will be shown and the prompt will not be modified.
251 251 In other words, it will be completely invisible to the user that
252 252 an execution has occurred.
253 253
254 254 interactive : bool, optional (default False)
255 255
256 256 Whether the console is to treat the source as having been manually
257 257 entered by the user. The effect of this parameter depends on the
258 258 subclass implementation.
259 259
260 260 Raises:
261 261 -------
262 262 RuntimeError
263 263 If incomplete input is given and 'hidden' is True. In this case,
264 264 it is not possible to prompt for more input.
265 265
266 266 Returns:
267 267 --------
268 268 A boolean indicating whether the source was executed.
269 269 """
270 270 if not hidden:
271 271 if source is not None:
272 272 self.input_buffer = source
273 273
274 274 self._append_plain_text('\n')
275 275 self._executing_input_buffer = self.input_buffer
276 276 self._executing = True
277 277 self._prompt_finished()
278 278
279 279 real_source = self.input_buffer if source is None else source
280 280 complete = self._is_complete(real_source, interactive)
281 281 if complete:
282 282 if not hidden:
283 283 # The maximum block count is only in effect during execution.
284 284 # This ensures that _prompt_pos does not become invalid due to
285 285 # text truncation.
286 286 self._control.document().setMaximumBlockCount(self.buffer_size)
287 287 self._execute(real_source, hidden)
288 288 elif hidden:
289 289 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
290 290 else:
291 291 self._show_continuation_prompt()
292 292
293 293 return complete
294 294
295 295 def _get_input_buffer(self):
296 296 """ The text that the user has entered entered at the current prompt.
297 297 """
298 298 # If we're executing, the input buffer may not even exist anymore due to
299 299 # the limit imposed by 'buffer_size'. Therefore, we store it.
300 300 if self._executing:
301 301 return self._executing_input_buffer
302 302
303 303 cursor = self._get_end_cursor()
304 304 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
305 305 input_buffer = str(cursor.selection().toPlainText())
306 306
307 307 # Strip out continuation prompts.
308 308 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
309 309
310 310 def _set_input_buffer(self, string):
311 311 """ Replaces the text in the input buffer with 'string'.
312 312 """
313 313 # For now, it is an error to modify the input buffer during execution.
314 314 if self._executing:
315 315 raise RuntimeError("Cannot change input buffer during execution.")
316 316
317 317 # Remove old text.
318 318 cursor = self._get_end_cursor()
319 319 cursor.beginEditBlock()
320 320 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
321 321 cursor.removeSelectedText()
322 322
323 323 # Insert new text with continuation prompts.
324 324 lines = string.splitlines(True)
325 325 if lines:
326 326 self._append_plain_text(lines[0])
327 327 for i in xrange(1, len(lines)):
328 328 if self._continuation_prompt_html is None:
329 329 self._append_plain_text(self._continuation_prompt)
330 330 else:
331 331 self._append_html(self._continuation_prompt_html)
332 332 self._append_plain_text(lines[i])
333 333 cursor.endEditBlock()
334 334 self._control.moveCursor(QtGui.QTextCursor.End)
335 335
336 336 input_buffer = property(_get_input_buffer, _set_input_buffer)
337 337
338 338 def _get_font(self):
339 339 """ The base font being used by the ConsoleWidget.
340 340 """
341 341 return self._control.document().defaultFont()
342 342
343 343 def _set_font(self, font):
344 344 """ Sets the base font for the ConsoleWidget to the specified QFont.
345 345 """
346 346 font_metrics = QtGui.QFontMetrics(font)
347 347 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
348 348
349 349 self._completion_widget.setFont(font)
350 350 self._control.document().setDefaultFont(font)
351 351 if self._page_control:
352 352 self._page_control.document().setDefaultFont(font)
353 353
354 354 font = property(_get_font, _set_font)
355 355
356 356 def paste(self):
357 357 """ Paste the contents of the clipboard into the input region.
358 358 """
359 359 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
360 360 try:
361 361 text = str(QtGui.QApplication.clipboard().text())
362 362 except UnicodeEncodeError:
363 363 pass
364 364 else:
365 365 self._insert_plain_text_into_buffer(dedent(text))
366 366
367 367 def print_(self, printer):
368 368 """ Print the contents of the ConsoleWidget to the specified QPrinter.
369 369 """
370 370 self._control.print_(printer)
371 371
372 372 def redo(self):
373 373 """ Redo the last operation. If there is no operation to redo, nothing
374 374 happens.
375 375 """
376 376 self._control.redo()
377 377
378 378 def reset_font(self):
379 379 """ Sets the font to the default fixed-width font for this platform.
380 380 """
381 381 if sys.platform == 'win32':
382 382 name = 'Courier'
383 383 elif sys.platform == 'darwin':
384 384 name = 'Monaco'
385 385 else:
386 386 name = 'Monospace'
387 387 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
388 388 font.setStyleHint(QtGui.QFont.TypeWriter)
389 389 self._set_font(font)
390 390
391 391 def select_all(self):
392 392 """ Selects all the text in the buffer.
393 393 """
394 394 self._control.selectAll()
395 395
396 396 def _get_tab_width(self):
397 397 """ The width (in terms of space characters) for tab characters.
398 398 """
399 399 return self._tab_width
400 400
401 401 def _set_tab_width(self, tab_width):
402 402 """ Sets the width (in terms of space characters) for tab characters.
403 403 """
404 404 font_metrics = QtGui.QFontMetrics(self.font)
405 405 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
406 406
407 407 self._tab_width = tab_width
408 408
409 409 tab_width = property(_get_tab_width, _set_tab_width)
410 410
411 411 def undo(self):
412 412 """ Undo the last operation. If there is no operation to undo, nothing
413 413 happens.
414 414 """
415 415 self._control.undo()
416 416
417 417 #---------------------------------------------------------------------------
418 418 # 'ConsoleWidget' abstract interface
419 419 #---------------------------------------------------------------------------
420 420
421 421 def _is_complete(self, source, interactive):
422 422 """ Returns whether 'source' can be executed. When triggered by an
423 423 Enter/Return key press, 'interactive' is True; otherwise, it is
424 424 False.
425 425 """
426 426 raise NotImplementedError
427 427
428 428 def _execute(self, source, hidden):
429 429 """ Execute 'source'. If 'hidden', do not show any output.
430 430 """
431 431 raise NotImplementedError
432 432
433 433 def _execute_interrupt(self):
434 434 """ Attempts to stop execution. Returns whether this method has an
435 435 implementation.
436 436 """
437 437 return False
438 438
439 439 def _prompt_started_hook(self):
440 440 """ Called immediately after a new prompt is displayed.
441 441 """
442 442 pass
443 443
444 444 def _prompt_finished_hook(self):
445 445 """ Called immediately after a prompt is finished, i.e. when some input
446 446 will be processed and a new prompt displayed.
447 447 """
448 448 pass
449 449
450 450 def _up_pressed(self):
451 451 """ Called when the up key is pressed. Returns whether to continue
452 452 processing the event.
453 453 """
454 454 return True
455 455
456 456 def _down_pressed(self):
457 457 """ Called when the down key is pressed. Returns whether to continue
458 458 processing the event.
459 459 """
460 460 return True
461 461
462 462 def _tab_pressed(self):
463 463 """ Called when the tab key is pressed. Returns whether to continue
464 464 processing the event.
465 465 """
466 466 return False
467 467
468 468 #--------------------------------------------------------------------------
469 469 # 'ConsoleWidget' protected interface
470 470 #--------------------------------------------------------------------------
471 471
472 472 def _append_html(self, html):
473 473 """ Appends html at the end of the console buffer.
474 474 """
475 475 cursor = self._get_end_cursor()
476 476 self._insert_html(cursor, html)
477 477
478 478 def _append_html_fetching_plain_text(self, html):
479 479 """ Appends 'html', then returns the plain text version of it.
480 480 """
481 481 cursor = self._get_end_cursor()
482 482 return self._insert_html_fetching_plain_text(cursor, html)
483 483
484 484 def _append_plain_text(self, text):
485 485 """ Appends plain text at the end of the console buffer, processing
486 486 ANSI codes if enabled.
487 487 """
488 488 cursor = self._get_end_cursor()
489 489 self._insert_plain_text(cursor, text)
490 490
491 491 def _append_plain_text_keeping_prompt(self, text):
492 492 """ Writes 'text' after the current prompt, then restores the old prompt
493 493 with its old input buffer.
494 494 """
495 495 input_buffer = self.input_buffer
496 496 self._append_plain_text('\n')
497 497 self._prompt_finished()
498 498
499 499 self._append_plain_text(text)
500 500 self._show_prompt()
501 501 self.input_buffer = input_buffer
502 502
503 503 def _complete_with_items(self, cursor, items):
504 504 """ Performs completion with 'items' at the specified cursor location.
505 505 """
506 506 if len(items) == 1:
507 507 cursor.setPosition(self._control.textCursor().position(),
508 508 QtGui.QTextCursor.KeepAnchor)
509 509 cursor.insertText(items[0])
510 510 elif len(items) > 1:
511 511 if self.gui_completion:
512 512 self._completion_widget.show_items(cursor, items)
513 513 else:
514 514 text = self._format_as_columns(items)
515 515 self._append_plain_text_keeping_prompt(text)
516 516
517 517 def _control_key_down(self, modifiers):
518 518 """ Given a KeyboardModifiers flags object, return whether the Control
519 519 key is down (on Mac OS, treat the Command key as a synonym for
520 520 Control).
521 521 """
522 522 down = bool(modifiers & QtCore.Qt.ControlModifier)
523 523
524 524 # Note: on Mac OS, ControlModifier corresponds to the Command key while
525 525 # MetaModifier corresponds to the Control key.
526 526 if sys.platform == 'darwin':
527 527 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
528 528
529 529 return down
530 530
531 531 def _create_control(self, kind):
532 532 """ Creates and connects the underlying text widget.
533 533 """
534 534 if kind == 'plain':
535 535 control = QtGui.QPlainTextEdit()
536 536 elif kind == 'rich':
537 537 control = QtGui.QTextEdit()
538 538 control.setAcceptRichText(False)
539 539 else:
540 540 raise ValueError("Kind %s unknown." % repr(kind))
541 541 control.installEventFilter(self)
542 542 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
543 543 control.customContextMenuRequested.connect(self._show_context_menu)
544 544 control.copyAvailable.connect(self.copy_available)
545 545 control.redoAvailable.connect(self.redo_available)
546 546 control.undoAvailable.connect(self.undo_available)
547 control.setReadOnly(True)
547 548 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
548 549 return control
549 550
550 551 def _create_page_control(self):
551 552 """ Creates and connects the underlying paging widget.
552 553 """
553 554 control = QtGui.QPlainTextEdit()
554 555 control.installEventFilter(self)
555 556 control.setReadOnly(True)
556 557 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
557 558 return control
558 559
559 560 def _event_filter_console_keypress(self, event):
560 561 """ Filter key events for the underlying text widget to create a
561 562 console-like interface.
562 563 """
563 564 intercepted = False
564 565 cursor = self._control.textCursor()
565 566 position = cursor.position()
566 567 key = event.key()
567 568 ctrl_down = self._control_key_down(event.modifiers())
568 569 alt_down = event.modifiers() & QtCore.Qt.AltModifier
569 570 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
570 571
571 572 if event.matches(QtGui.QKeySequence.Paste):
572 573 # Call our paste instead of the underlying text widget's.
573 574 self.paste()
574 575 intercepted = True
575 576
576 577 elif ctrl_down:
577 578 if key == QtCore.Qt.Key_C:
578 579 intercepted = self._executing and self._execute_interrupt()
579 580
580 581 elif key == QtCore.Qt.Key_K:
581 582 if self._in_buffer(position):
582 583 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
583 584 QtGui.QTextCursor.KeepAnchor)
584 585 cursor.removeSelectedText()
585 586 intercepted = True
586 587
587 588 elif key == QtCore.Qt.Key_X:
588 589 intercepted = True
589 590
590 591 elif key == QtCore.Qt.Key_Y:
591 592 self.paste()
592 593 intercepted = True
593 594
594 595 elif alt_down:
595 596 if key == QtCore.Qt.Key_B:
596 597 self._set_cursor(self._get_word_start_cursor(position))
597 598 intercepted = True
598 599
599 600 elif key == QtCore.Qt.Key_F:
600 601 self._set_cursor(self._get_word_end_cursor(position))
601 602 intercepted = True
602 603
603 604 elif key == QtCore.Qt.Key_Backspace:
604 605 cursor = self._get_word_start_cursor(position)
605 606 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
606 607 cursor.removeSelectedText()
607 608 intercepted = True
608 609
609 610 elif key == QtCore.Qt.Key_D:
610 611 cursor = self._get_word_end_cursor(position)
611 612 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
612 613 cursor.removeSelectedText()
613 614 intercepted = True
614 615
615 616 else:
616 617 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
617 618 if self._reading:
618 619 self._append_plain_text('\n')
619 620 self._reading = False
620 621 if self._reading_callback:
621 622 self._reading_callback()
622 623 elif not self._executing:
623 624 self.execute(interactive=True)
624 625 intercepted = True
625 626
626 627 elif key == QtCore.Qt.Key_Up:
627 628 if self._reading or not self._up_pressed():
628 629 intercepted = True
629 630 else:
630 631 prompt_line = self._get_prompt_cursor().blockNumber()
631 632 intercepted = cursor.blockNumber() <= prompt_line
632 633
633 634 elif key == QtCore.Qt.Key_Down:
634 635 if self._reading or not self._down_pressed():
635 636 intercepted = True
636 637 else:
637 638 end_line = self._get_end_cursor().blockNumber()
638 639 intercepted = cursor.blockNumber() == end_line
639 640
640 641 elif key == QtCore.Qt.Key_Tab:
641 642 if self._reading:
642 643 intercepted = False
643 644 else:
644 645 intercepted = not self._tab_pressed()
645 646
646 647 elif key == QtCore.Qt.Key_Left:
647 648 intercepted = not self._in_buffer(position - 1)
648 649
649 650 elif key == QtCore.Qt.Key_Home:
650 651 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
651 652 start_line = cursor.blockNumber()
652 653 if start_line == self._get_prompt_cursor().blockNumber():
653 654 start_pos = self._prompt_pos
654 655 else:
655 656 start_pos = cursor.position()
656 657 start_pos += len(self._continuation_prompt)
657 658 if shift_down and self._in_buffer(position):
658 659 self._set_selection(position, start_pos)
659 660 else:
660 661 self._set_position(start_pos)
661 662 intercepted = True
662 663
663 664 elif key == QtCore.Qt.Key_Backspace:
664 665
665 666 # Line deletion (remove continuation prompt)
666 667 len_prompt = len(self._continuation_prompt)
667 668 if not self._reading and \
668 669 cursor.columnNumber() == len_prompt and \
669 670 position != self._prompt_pos:
670 671 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
671 672 QtGui.QTextCursor.KeepAnchor)
672 673 cursor.removeSelectedText()
673 674 cursor.deletePreviousChar()
674 675 intercepted = True
675 676
676 677 # Regular backwards deletion
677 678 else:
678 679 anchor = cursor.anchor()
679 680 if anchor == position:
680 681 intercepted = not self._in_buffer(position - 1)
681 682 else:
682 683 intercepted = not self._in_buffer(min(anchor, position))
683 684
684 685 elif key == QtCore.Qt.Key_Delete:
685 686 anchor = cursor.anchor()
686 687 intercepted = not self._in_buffer(min(anchor, position))
687 688
688 689 # Don't move the cursor if control is down to allow copy-paste using
689 690 # the keyboard in any part of the buffer.
690 691 if not ctrl_down:
691 692 self._keep_cursor_in_buffer()
692 693
693 694 return intercepted
694 695
695 696 def _event_filter_page_keypress(self, event):
696 697 """ Filter key events for the paging widget to create console-like
697 698 interface.
698 699 """
699 700 key = event.key()
700 701
701 702 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
702 703 if self._splitter:
703 704 self._page_control.hide()
704 705 else:
705 706 self.layout().setCurrentWidget(self._control)
706 707 return True
707 708
708 709 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
709 710 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
710 711 QtCore.Qt.Key_Down,
711 712 QtCore.Qt.NoModifier)
712 713 QtGui.qApp.sendEvent(self._page_control, new_event)
713 714 return True
714 715
715 716 return False
716 717
717 718 def _format_as_columns(self, items, separator=' '):
718 719 """ Transform a list of strings into a single string with columns.
719 720
720 721 Parameters
721 722 ----------
722 723 items : sequence of strings
723 724 The strings to process.
724 725
725 726 separator : str, optional [default is two spaces]
726 727 The string that separates columns.
727 728
728 729 Returns
729 730 -------
730 731 The formatted string.
731 732 """
732 733 # Note: this code is adapted from columnize 0.3.2.
733 734 # See http://code.google.com/p/pycolumnize/
734 735
735 736 width = self._control.viewport().width()
736 737 char_width = QtGui.QFontMetrics(self.font).width(' ')
737 738 displaywidth = max(5, width / char_width)
738 739
739 740 # Some degenerate cases.
740 741 size = len(items)
741 742 if size == 0:
742 743 return '\n'
743 744 elif size == 1:
744 745 return '%s\n' % str(items[0])
745 746
746 747 # Try every row count from 1 upwards
747 748 array_index = lambda nrows, row, col: nrows*col + row
748 749 for nrows in range(1, size):
749 750 ncols = (size + nrows - 1) // nrows
750 751 colwidths = []
751 752 totwidth = -len(separator)
752 753 for col in range(ncols):
753 754 # Get max column width for this column
754 755 colwidth = 0
755 756 for row in range(nrows):
756 757 i = array_index(nrows, row, col)
757 758 if i >= size: break
758 759 x = items[i]
759 760 colwidth = max(colwidth, len(x))
760 761 colwidths.append(colwidth)
761 762 totwidth += colwidth + len(separator)
762 763 if totwidth > displaywidth:
763 764 break
764 765 if totwidth <= displaywidth:
765 766 break
766 767
767 768 # The smallest number of rows computed and the max widths for each
768 769 # column has been obtained. Now we just have to format each of the rows.
769 770 string = ''
770 771 for row in range(nrows):
771 772 texts = []
772 773 for col in range(ncols):
773 774 i = row + nrows*col
774 775 if i >= size:
775 776 texts.append('')
776 777 else:
777 778 texts.append(items[i])
778 779 while texts and not texts[-1]:
779 780 del texts[-1]
780 781 for col in range(len(texts)):
781 782 texts[col] = texts[col].ljust(colwidths[col])
782 783 string += '%s\n' % str(separator.join(texts))
783 784 return string
784 785
785 786 def _get_block_plain_text(self, block):
786 787 """ Given a QTextBlock, return its unformatted text.
787 788 """
788 789 cursor = QtGui.QTextCursor(block)
789 790 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
790 791 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
791 792 QtGui.QTextCursor.KeepAnchor)
792 793 return str(cursor.selection().toPlainText())
793 794
794 795 def _get_cursor(self):
795 796 """ Convenience method that returns a cursor for the current position.
796 797 """
797 798 return self._control.textCursor()
798 799
799 800 def _get_end_cursor(self):
800 801 """ Convenience method that returns a cursor for the last character.
801 802 """
802 803 cursor = self._control.textCursor()
803 804 cursor.movePosition(QtGui.QTextCursor.End)
804 805 return cursor
805 806
806 807 def _get_input_buffer_cursor_column(self):
807 808 """ Returns the column of the cursor in the input buffer, excluding the
808 809 contribution by the prompt, or -1 if there is no such column.
809 810 """
810 811 prompt = self._get_input_buffer_cursor_prompt()
811 812 if prompt is None:
812 813 return -1
813 814 else:
814 815 cursor = self._control.textCursor()
815 816 return cursor.columnNumber() - len(prompt)
816 817
817 818 def _get_input_buffer_cursor_line(self):
818 819 """ Returns line of the input buffer that contains the cursor, or None
819 820 if there is no such line.
820 821 """
821 822 prompt = self._get_input_buffer_cursor_prompt()
822 823 if prompt is None:
823 824 return None
824 825 else:
825 826 cursor = self._control.textCursor()
826 827 text = self._get_block_plain_text(cursor.block())
827 828 return text[len(prompt):]
828 829
829 830 def _get_input_buffer_cursor_prompt(self):
830 831 """ Returns the (plain text) prompt for line of the input buffer that
831 832 contains the cursor, or None if there is no such line.
832 833 """
833 834 if self._executing:
834 835 return None
835 836 cursor = self._control.textCursor()
836 837 if cursor.position() >= self._prompt_pos:
837 838 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
838 839 return self._prompt
839 840 else:
840 841 return self._continuation_prompt
841 842 else:
842 843 return None
843 844
844 845 def _get_prompt_cursor(self):
845 846 """ Convenience method that returns a cursor for the prompt position.
846 847 """
847 848 cursor = self._control.textCursor()
848 849 cursor.setPosition(self._prompt_pos)
849 850 return cursor
850 851
851 852 def _get_selection_cursor(self, start, end):
852 853 """ Convenience method that returns a cursor with text selected between
853 854 the positions 'start' and 'end'.
854 855 """
855 856 cursor = self._control.textCursor()
856 857 cursor.setPosition(start)
857 858 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
858 859 return cursor
859 860
860 861 def _get_word_start_cursor(self, position):
861 862 """ Find the start of the word to the left the given position. If a
862 863 sequence of non-word characters precedes the first word, skip over
863 864 them. (This emulates the behavior of bash, emacs, etc.)
864 865 """
865 866 document = self._control.document()
866 867 position -= 1
867 868 while position >= self._prompt_pos and \
868 869 not document.characterAt(position).isLetterOrNumber():
869 870 position -= 1
870 871 while position >= self._prompt_pos and \
871 872 document.characterAt(position).isLetterOrNumber():
872 873 position -= 1
873 874 cursor = self._control.textCursor()
874 875 cursor.setPosition(position + 1)
875 876 return cursor
876 877
877 878 def _get_word_end_cursor(self, position):
878 879 """ Find the end of the word to the right the given position. If a
879 880 sequence of non-word characters precedes the first word, skip over
880 881 them. (This emulates the behavior of bash, emacs, etc.)
881 882 """
882 883 document = self._control.document()
883 884 end = self._get_end_cursor().position()
884 885 while position < end and \
885 886 not document.characterAt(position).isLetterOrNumber():
886 887 position += 1
887 888 while position < end and \
888 889 document.characterAt(position).isLetterOrNumber():
889 890 position += 1
890 891 cursor = self._control.textCursor()
891 892 cursor.setPosition(position)
892 893 return cursor
893 894
894 895 def _insert_html(self, cursor, html):
895 896 """ Inserts HTML using the specified cursor in such a way that future
896 897 formatting is unaffected.
897 898 """
898 899 cursor.beginEditBlock()
899 900 cursor.insertHtml(html)
900 901
901 902 # After inserting HTML, the text document "remembers" it's in "html
902 903 # mode", which means that subsequent calls adding plain text will result
903 904 # in unwanted formatting, lost tab characters, etc. The following code
904 905 # hacks around this behavior, which I consider to be a bug in Qt, by
905 906 # (crudely) resetting the document's style state.
906 907 cursor.movePosition(QtGui.QTextCursor.Left,
907 908 QtGui.QTextCursor.KeepAnchor)
908 909 if cursor.selection().toPlainText() == ' ':
909 910 cursor.removeSelectedText()
910 911 else:
911 912 cursor.movePosition(QtGui.QTextCursor.Right)
912 913 cursor.insertText(' ', QtGui.QTextCharFormat())
913 914 cursor.endEditBlock()
914 915
915 916 def _insert_html_fetching_plain_text(self, cursor, html):
916 917 """ Inserts HTML using the specified cursor, then returns its plain text
917 918 version.
918 919 """
919 920 cursor.beginEditBlock()
920 921 cursor.removeSelectedText()
921 922
922 923 start = cursor.position()
923 924 self._insert_html(cursor, html)
924 925 end = cursor.position()
925 926 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
926 927 text = str(cursor.selection().toPlainText())
927 928
928 929 cursor.setPosition(end)
929 930 cursor.endEditBlock()
930 931 return text
931 932
932 933 def _insert_plain_text(self, cursor, text):
933 934 """ Inserts plain text using the specified cursor, processing ANSI codes
934 935 if enabled.
935 936 """
936 937 cursor.beginEditBlock()
937 938 if self.ansi_codes:
938 939 for substring in self._ansi_processor.split_string(text):
939 940 for action in self._ansi_processor.actions:
940 941 if action.kind == 'erase' and action.area == 'screen':
941 942 cursor.select(QtGui.QTextCursor.Document)
942 943 cursor.removeSelectedText()
943 944 format = self._ansi_processor.get_format()
944 945 cursor.insertText(substring, format)
945 946 else:
946 947 cursor.insertText(text)
947 948 cursor.endEditBlock()
948 949
949 950 def _insert_plain_text_into_buffer(self, text):
950 951 """ Inserts text into the input buffer at the current cursor position,
951 952 ensuring that continuation prompts are inserted as necessary.
952 953 """
953 954 lines = str(text).splitlines(True)
954 955 if lines:
955 956 self._keep_cursor_in_buffer()
956 957 cursor = self._control.textCursor()
957 958 cursor.beginEditBlock()
958 959 cursor.insertText(lines[0])
959 960 for line in lines[1:]:
960 961 if self._continuation_prompt_html is None:
961 962 cursor.insertText(self._continuation_prompt)
962 963 else:
963 964 self._continuation_prompt = \
964 965 self._insert_html_fetching_plain_text(
965 966 cursor, self._continuation_prompt_html)
966 967 cursor.insertText(line)
967 968 cursor.endEditBlock()
968 969 self._control.setTextCursor(cursor)
969 970
970 971 def _in_buffer(self, position):
971 972 """ Returns whether the given position is inside the editing region.
972 973 """
973 974 cursor = self._control.textCursor()
974 975 cursor.setPosition(position)
975 976 line = cursor.blockNumber()
976 977 prompt_line = self._get_prompt_cursor().blockNumber()
977 978 if line == prompt_line:
978 979 return position >= self._prompt_pos
979 980 elif line > prompt_line:
980 981 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
981 982 prompt_pos = cursor.position() + len(self._continuation_prompt)
982 983 return position >= prompt_pos
983 984 return False
984 985
985 986 def _keep_cursor_in_buffer(self):
986 987 """ Ensures that the cursor is inside the editing region. Returns
987 988 whether the cursor was moved.
988 989 """
989 990 cursor = self._control.textCursor()
990 991 if self._in_buffer(cursor.position()):
991 992 return False
992 993 else:
993 994 cursor.movePosition(QtGui.QTextCursor.End)
994 995 self._control.setTextCursor(cursor)
995 996 return True
996 997
997 998 def _page(self, text):
998 999 """ Displays text using the pager if it exceeds the height of the
999 1000 visible area.
1000 1001 """
1001 1002 if self._page_style == 'none':
1002 1003 self._append_plain_text(text)
1003 1004 else:
1004 1005 line_height = QtGui.QFontMetrics(self.font).height()
1005 1006 minlines = self._control.viewport().height() / line_height
1006 1007 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1007 1008 if self._page_style == 'custom':
1008 1009 self.custom_page_requested.emit(text)
1009 1010 else:
1010 1011 self._page_control.clear()
1011 1012 cursor = self._page_control.textCursor()
1012 1013 self._insert_plain_text(cursor, text)
1013 1014 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1014 1015
1015 1016 self._page_control.viewport().resize(self._control.size())
1016 1017 if self._splitter:
1017 1018 self._page_control.show()
1018 1019 self._page_control.setFocus()
1019 1020 else:
1020 1021 self.layout().setCurrentWidget(self._page_control)
1021 1022 else:
1022 1023 self._append_plain_text(text)
1023 1024
1024 1025 def _prompt_started(self):
1025 1026 """ Called immediately after a new prompt is displayed.
1026 1027 """
1027 1028 # Temporarily disable the maximum block count to permit undo/redo and
1028 1029 # to ensure that the prompt position does not change due to truncation.
1029 1030 self._control.document().setMaximumBlockCount(0)
1030 1031 self._control.setUndoRedoEnabled(True)
1031 1032
1032 1033 self._control.setReadOnly(False)
1033 1034 self._control.moveCursor(QtGui.QTextCursor.End)
1034 1035
1035 1036 self._executing = False
1036 1037 self._prompt_started_hook()
1037 1038
1038 1039 def _prompt_finished(self):
1039 1040 """ Called immediately after a prompt is finished, i.e. when some input
1040 1041 will be processed and a new prompt displayed.
1041 1042 """
1042 1043 self._control.setUndoRedoEnabled(False)
1043 1044 self._control.setReadOnly(True)
1044 1045 self._prompt_finished_hook()
1045 1046
1046 1047 def _readline(self, prompt='', callback=None):
1047 1048 """ Reads one line of input from the user.
1048 1049
1049 1050 Parameters
1050 1051 ----------
1051 1052 prompt : str, optional
1052 1053 The prompt to print before reading the line.
1053 1054
1054 1055 callback : callable, optional
1055 1056 A callback to execute with the read line. If not specified, input is
1056 1057 read *synchronously* and this method does not return until it has
1057 1058 been read.
1058 1059
1059 1060 Returns
1060 1061 -------
1061 1062 If a callback is specified, returns nothing. Otherwise, returns the
1062 1063 input string with the trailing newline stripped.
1063 1064 """
1064 1065 if self._reading:
1065 1066 raise RuntimeError('Cannot read a line. Widget is already reading.')
1066 1067
1067 1068 if not callback and not self.isVisible():
1068 1069 # If the user cannot see the widget, this function cannot return.
1069 1070 raise RuntimeError('Cannot synchronously read a line if the widget'
1070 1071 'is not visible!')
1071 1072
1072 1073 self._reading = True
1073 1074 self._show_prompt(prompt, newline=False)
1074 1075
1075 1076 if callback is None:
1076 1077 self._reading_callback = None
1077 1078 while self._reading:
1078 1079 QtCore.QCoreApplication.processEvents()
1079 1080 return self.input_buffer.rstrip('\n')
1080 1081
1081 1082 else:
1082 1083 self._reading_callback = lambda: \
1083 1084 callback(self.input_buffer.rstrip('\n'))
1084 1085
1085 1086 def _set_continuation_prompt(self, prompt, html=False):
1086 1087 """ Sets the continuation prompt.
1087 1088
1088 1089 Parameters
1089 1090 ----------
1090 1091 prompt : str
1091 1092 The prompt to show when more input is needed.
1092 1093
1093 1094 html : bool, optional (default False)
1094 1095 If set, the prompt will be inserted as formatted HTML. Otherwise,
1095 1096 the prompt will be treated as plain text, though ANSI color codes
1096 1097 will be handled.
1097 1098 """
1098 1099 if html:
1099 1100 self._continuation_prompt_html = prompt
1100 1101 else:
1101 1102 self._continuation_prompt = prompt
1102 1103 self._continuation_prompt_html = None
1103 1104
1104 1105 def _set_cursor(self, cursor):
1105 1106 """ Convenience method to set the current cursor.
1106 1107 """
1107 1108 self._control.setTextCursor(cursor)
1108 1109
1109 1110 def _set_position(self, position):
1110 1111 """ Convenience method to set the position of the cursor.
1111 1112 """
1112 1113 cursor = self._control.textCursor()
1113 1114 cursor.setPosition(position)
1114 1115 self._control.setTextCursor(cursor)
1115 1116
1116 1117 def _set_selection(self, start, end):
1117 1118 """ Convenience method to set the current selected text.
1118 1119 """
1119 1120 self._control.setTextCursor(self._get_selection_cursor(start, end))
1120 1121
1121 1122 def _show_context_menu(self, pos):
1122 1123 """ Shows a context menu at the given QPoint (in widget coordinates).
1123 1124 """
1124 1125 menu = QtGui.QMenu()
1125 1126
1126 1127 copy_action = menu.addAction('Copy', self.copy)
1127 1128 copy_action.setEnabled(self._get_cursor().hasSelection())
1128 1129 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1129 1130
1130 1131 paste_action = menu.addAction('Paste', self.paste)
1131 1132 paste_action.setEnabled(self.can_paste())
1132 1133 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1133 1134
1134 1135 menu.addSeparator()
1135 1136 menu.addAction('Select All', self.select_all)
1136 1137
1137 1138 menu.exec_(self._control.mapToGlobal(pos))
1138 1139
1139 1140 def _show_prompt(self, prompt=None, html=False, newline=True):
1140 1141 """ Writes a new prompt at the end of the buffer.
1141 1142
1142 1143 Parameters
1143 1144 ----------
1144 1145 prompt : str, optional
1145 1146 The prompt to show. If not specified, the previous prompt is used.
1146 1147
1147 1148 html : bool, optional (default False)
1148 1149 Only relevant when a prompt is specified. If set, the prompt will
1149 1150 be inserted as formatted HTML. Otherwise, the prompt will be treated
1150 1151 as plain text, though ANSI color codes will be handled.
1151 1152
1152 1153 newline : bool, optional (default True)
1153 1154 If set, a new line will be written before showing the prompt if
1154 1155 there is not already a newline at the end of the buffer.
1155 1156 """
1156 1157 # Insert a preliminary newline, if necessary.
1157 1158 if newline:
1158 1159 cursor = self._get_end_cursor()
1159 1160 if cursor.position() > 0:
1160 1161 cursor.movePosition(QtGui.QTextCursor.Left,
1161 1162 QtGui.QTextCursor.KeepAnchor)
1162 1163 if str(cursor.selection().toPlainText()) != '\n':
1163 1164 self._append_plain_text('\n')
1164 1165
1165 1166 # Write the prompt.
1166 1167 if prompt is None:
1167 1168 if self._prompt_html is None:
1168 1169 self._append_plain_text(self._prompt)
1169 1170 else:
1170 1171 self._append_html(self._prompt_html)
1171 1172 else:
1172 1173 if html:
1173 1174 self._prompt = self._append_html_fetching_plain_text(prompt)
1174 1175 self._prompt_html = prompt
1175 1176 else:
1176 1177 self._append_plain_text(prompt)
1177 1178 self._prompt = prompt
1178 1179 self._prompt_html = None
1179 1180
1180 1181 self._prompt_pos = self._get_end_cursor().position()
1181 1182 self._prompt_started()
1182 1183
1183 1184 def _show_continuation_prompt(self):
1184 1185 """ Writes a new continuation prompt at the end of the buffer.
1185 1186 """
1186 1187 if self._continuation_prompt_html is None:
1187 1188 self._append_plain_text(self._continuation_prompt)
1188 1189 else:
1189 1190 self._continuation_prompt = self._append_html_fetching_plain_text(
1190 1191 self._continuation_prompt_html)
1191 1192
1192 1193 self._prompt_started()
1193 1194
1194 1195
1195 1196 class HistoryConsoleWidget(ConsoleWidget):
1196 1197 """ A ConsoleWidget that keeps a history of the commands that have been
1197 1198 executed.
1198 1199 """
1199 1200
1200 1201 #---------------------------------------------------------------------------
1201 1202 # 'object' interface
1202 1203 #---------------------------------------------------------------------------
1203 1204
1204 1205 def __init__(self, *args, **kw):
1205 1206 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1206 1207 self._history = []
1207 1208 self._history_index = 0
1208 1209
1209 1210 #---------------------------------------------------------------------------
1210 1211 # 'ConsoleWidget' public interface
1211 1212 #---------------------------------------------------------------------------
1212 1213
1213 1214 def execute(self, source=None, hidden=False, interactive=False):
1214 1215 """ Reimplemented to the store history.
1215 1216 """
1216 1217 if not hidden:
1217 1218 history = self.input_buffer if source is None else source
1218 1219
1219 1220 executed = super(HistoryConsoleWidget, self).execute(
1220 1221 source, hidden, interactive)
1221 1222
1222 1223 if executed and not hidden:
1223 1224 self._history.append(history.rstrip())
1224 1225 self._history_index = len(self._history)
1225 1226
1226 1227 return executed
1227 1228
1228 1229 #---------------------------------------------------------------------------
1229 1230 # 'ConsoleWidget' abstract interface
1230 1231 #---------------------------------------------------------------------------
1231 1232
1232 1233 def _up_pressed(self):
1233 1234 """ Called when the up key is pressed. Returns whether to continue
1234 1235 processing the event.
1235 1236 """
1236 1237 prompt_cursor = self._get_prompt_cursor()
1237 1238 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1238 1239 self.history_previous()
1239 1240
1240 1241 # Go to the first line of prompt for seemless history scrolling.
1241 1242 cursor = self._get_prompt_cursor()
1242 1243 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1243 1244 self._set_cursor(cursor)
1244 1245
1245 1246 return False
1246 1247 return True
1247 1248
1248 1249 def _down_pressed(self):
1249 1250 """ Called when the down key is pressed. Returns whether to continue
1250 1251 processing the event.
1251 1252 """
1252 1253 end_cursor = self._get_end_cursor()
1253 1254 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1254 1255 self.history_next()
1255 1256 return False
1256 1257 return True
1257 1258
1258 1259 #---------------------------------------------------------------------------
1259 # 'HistoryConsoleWidget' interface
1260 # 'HistoryConsoleWidget' public interface
1260 1261 #---------------------------------------------------------------------------
1261 1262
1262 1263 def history_previous(self):
1263 1264 """ If possible, set the input buffer to the previous item in the
1264 1265 history.
1265 1266 """
1266 1267 if self._history_index > 0:
1267 1268 self._history_index -= 1
1268 1269 self.input_buffer = self._history[self._history_index]
1269 1270
1270 1271 def history_next(self):
1271 1272 """ Set the input buffer to the next item in the history, or a blank
1272 1273 line if there is no subsequent item.
1273 1274 """
1274 1275 if self._history_index < len(self._history):
1275 1276 self._history_index += 1
1276 1277 if self._history_index < len(self._history):
1277 1278 self.input_buffer = self._history[self._history_index]
1278 1279 else:
1279 1280 self.input_buffer = ''
1281
1282 #---------------------------------------------------------------------------
1283 # 'HistoryConsoleWidget' protected interface
1284 #---------------------------------------------------------------------------
1285
1286 def _set_history(self, history):
1287 """ Replace the current history with a sequence of history items.
1288 """
1289 self._history = list(history)
1290 self._history_index = len(self._history)
@@ -1,379 +1,379 b''
1 1 # Standard library imports
2 2 import signal
3 3 import sys
4 4
5 5 # System library imports
6 6 from pygments.lexers import PythonLexer
7 7 from PyQt4 import QtCore, QtGui
8 8 import zmq
9 9
10 10 # Local imports
11 11 from IPython.core.inputsplitter import InputSplitter
12 12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
13 13 from call_tip_widget import CallTipWidget
14 14 from completion_lexer import CompletionLexer
15 15 from console_widget import HistoryConsoleWidget
16 16 from pygments_highlighter import PygmentsHighlighter
17 17
18 18
19 19 class FrontendHighlighter(PygmentsHighlighter):
20 20 """ A PygmentsHighlighter that can be turned on and off and that ignores
21 21 prompts.
22 22 """
23 23
24 24 def __init__(self, frontend):
25 25 super(FrontendHighlighter, self).__init__(frontend._control.document())
26 26 self._current_offset = 0
27 27 self._frontend = frontend
28 28 self.highlighting_on = False
29 29
30 30 def highlightBlock(self, qstring):
31 31 """ Highlight a block of text. Reimplemented to highlight selectively.
32 32 """
33 33 if not self.highlighting_on:
34 34 return
35 35
36 36 # The input to this function is unicode string that may contain
37 37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
38 38 # the string as plain text so we can compare it.
39 39 current_block = self.currentBlock()
40 40 string = self._frontend._get_block_plain_text(current_block)
41 41
42 42 # Decide whether to check for the regular or continuation prompt.
43 43 if current_block.contains(self._frontend._prompt_pos):
44 44 prompt = self._frontend._prompt
45 45 else:
46 46 prompt = self._frontend._continuation_prompt
47 47
48 48 # Don't highlight the part of the string that contains the prompt.
49 49 if string.startswith(prompt):
50 50 self._current_offset = len(prompt)
51 51 qstring.remove(0, len(prompt))
52 52 else:
53 53 self._current_offset = 0
54 54
55 55 PygmentsHighlighter.highlightBlock(self, qstring)
56 56
57 57 def rehighlightBlock(self, block):
58 58 """ Reimplemented to temporarily enable highlighting if disabled.
59 59 """
60 60 old = self.highlighting_on
61 61 self.highlighting_on = True
62 62 super(FrontendHighlighter, self).rehighlightBlock(block)
63 63 self.highlighting_on = old
64 64
65 65 def setFormat(self, start, count, format):
66 66 """ Reimplemented to highlight selectively.
67 67 """
68 68 start += self._current_offset
69 69 PygmentsHighlighter.setFormat(self, start, count, format)
70 70
71 71
72 72 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
73 73 """ A Qt frontend for a generic Python kernel.
74 74 """
75 75
76 76 # Emitted when an 'execute_reply' has been received from the kernel and
77 77 # processed by the FrontendWidget.
78 78 executed = QtCore.pyqtSignal(object)
79 79
80 80 # Protected class attributes.
81 81 _highlighter_class = FrontendHighlighter
82 82 _input_splitter_class = InputSplitter
83 83
84 84 #---------------------------------------------------------------------------
85 85 # 'object' interface
86 86 #---------------------------------------------------------------------------
87 87
88 88 def __init__(self, *args, **kw):
89 89 super(FrontendWidget, self).__init__(*args, **kw)
90 90
91 91 # FrontendWidget protected variables.
92 92 self._call_tip_widget = CallTipWidget(self._control)
93 93 self._completion_lexer = CompletionLexer(PythonLexer())
94 94 self._hidden = False
95 95 self._highlighter = self._highlighter_class(self)
96 96 self._input_splitter = self._input_splitter_class(input_mode='replace')
97 97 self._kernel_manager = None
98 98
99 99 # Configure the ConsoleWidget.
100 100 self.tab_width = 4
101 101 self._set_continuation_prompt('... ')
102 102
103 103 # Connect signal handlers.
104 104 document = self._control.document()
105 105 document.contentsChange.connect(self._document_contents_change)
106 106
107 107 #---------------------------------------------------------------------------
108 108 # 'ConsoleWidget' abstract interface
109 109 #---------------------------------------------------------------------------
110 110
111 111 def _is_complete(self, source, interactive):
112 112 """ Returns whether 'source' can be completely processed and a new
113 113 prompt created. When triggered by an Enter/Return key press,
114 114 'interactive' is True; otherwise, it is False.
115 115 """
116 116 complete = self._input_splitter.push(source.expandtabs(4))
117 117 if interactive:
118 118 complete = not self._input_splitter.push_accepts_more()
119 119 return complete
120 120
121 121 def _execute(self, source, hidden):
122 122 """ Execute 'source'. If 'hidden', do not show any output.
123 123 """
124 self.kernel_manager.xreq_channel.execute(source)
124 self.kernel_manager.xreq_channel.execute(source, hidden)
125 125 self._hidden = hidden
126 126
127 127 def _execute_interrupt(self):
128 128 """ Attempts to stop execution. Returns whether this method has an
129 129 implementation.
130 130 """
131 131 self._interrupt_kernel()
132 132 return True
133 133
134 134 def _prompt_started_hook(self):
135 135 """ Called immediately after a new prompt is displayed.
136 136 """
137 137 if not self._reading:
138 138 self._highlighter.highlighting_on = True
139 139
140 140 def _prompt_finished_hook(self):
141 141 """ Called immediately after a prompt is finished, i.e. when some input
142 142 will be processed and a new prompt displayed.
143 143 """
144 144 if not self._reading:
145 145 self._highlighter.highlighting_on = False
146 146
147 147 def _tab_pressed(self):
148 148 """ Called when the tab key is pressed. Returns whether to continue
149 149 processing the event.
150 150 """
151 151 self._keep_cursor_in_buffer()
152 152 cursor = self._get_cursor()
153 153 return not self._complete()
154 154
155 155 #---------------------------------------------------------------------------
156 156 # 'ConsoleWidget' protected interface
157 157 #---------------------------------------------------------------------------
158 158
159 159 def _show_continuation_prompt(self):
160 160 """ Reimplemented for auto-indentation.
161 161 """
162 162 super(FrontendWidget, self)._show_continuation_prompt()
163 163 spaces = self._input_splitter.indent_spaces
164 164 self._append_plain_text('\t' * (spaces / self.tab_width))
165 165 self._append_plain_text(' ' * (spaces % self.tab_width))
166 166
167 167 #---------------------------------------------------------------------------
168 168 # 'BaseFrontendMixin' abstract interface
169 169 #---------------------------------------------------------------------------
170 170
171 171 def _handle_complete_reply(self, rep):
172 172 """ Handle replies for tab completion.
173 173 """
174 174 cursor = self._get_cursor()
175 175 if rep['parent_header']['msg_id'] == self._complete_id and \
176 176 cursor.position() == self._complete_pos:
177 177 text = '.'.join(self._get_context())
178 178 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
179 179 self._complete_with_items(cursor, rep['content']['matches'])
180 180
181 181 def _handle_execute_reply(self, msg):
182 182 """ Handles replies for code execution.
183 183 """
184 184 if not self._hidden:
185 185 # Make sure that all output from the SUB channel has been processed
186 186 # before writing a new prompt.
187 187 self.kernel_manager.sub_channel.flush()
188 188
189 189 content = msg['content']
190 190 status = content['status']
191 191 if status == 'ok':
192 192 self._process_execute_ok(msg)
193 193 elif status == 'error':
194 194 self._process_execute_error(msg)
195 195 elif status == 'abort':
196 196 self._process_execute_abort(msg)
197 197
198 198 self._show_interpreter_prompt_for_reply(msg)
199 199 self.executed.emit(msg)
200 200
201 201 def _handle_input_request(self, msg):
202 202 """ Handle requests for raw_input.
203 203 """
204 204 if self._hidden:
205 205 raise RuntimeError('Request for raw input during hidden execution.')
206 206
207 207 # Make sure that all output from the SUB channel has been processed
208 208 # before entering readline mode.
209 209 self.kernel_manager.sub_channel.flush()
210 210
211 211 def callback(line):
212 212 self.kernel_manager.rep_channel.input(line)
213 213 self._readline(msg['content']['prompt'], callback=callback)
214 214
215 215 def _handle_object_info_reply(self, rep):
216 216 """ Handle replies for call tips.
217 217 """
218 218 cursor = self._get_cursor()
219 219 if rep['parent_header']['msg_id'] == self._call_tip_id and \
220 220 cursor.position() == self._call_tip_pos:
221 221 doc = rep['content']['docstring']
222 222 if doc:
223 223 self._call_tip_widget.show_docstring(doc)
224 224
225 225 def _handle_pyout(self, msg):
226 226 """ Handle display hook output.
227 227 """
228 228 if not self._hidden and self._is_from_this_session(msg):
229 229 self._append_plain_text(msg['content']['data'] + '\n')
230 230
231 231 def _handle_stream(self, msg):
232 232 """ Handle stdout, stderr, and stdin.
233 233 """
234 234 if not self._hidden and self._is_from_this_session(msg):
235 235 self._append_plain_text(msg['content']['data'])
236 236 self._control.moveCursor(QtGui.QTextCursor.End)
237 237
238 238 def _started_channels(self):
239 239 """ Called when the KernelManager channels have started listening or
240 240 when the frontend is assigned an already listening KernelManager.
241 241 """
242 242 self._control.clear()
243 243 self._append_plain_text(self._get_banner())
244 244 self._show_interpreter_prompt()
245 245
246 246 def _stopped_channels(self):
247 247 """ Called when the KernelManager channels have stopped listening or
248 248 when a listening KernelManager is removed from the frontend.
249 249 """
250 250 self._executing = self._reading = False
251 251 self._highlighter.highlighting_on = False
252 252
253 253 #---------------------------------------------------------------------------
254 254 # 'FrontendWidget' interface
255 255 #---------------------------------------------------------------------------
256 256
257 257 def execute_file(self, path, hidden=False):
258 258 """ Attempts to execute file with 'path'. If 'hidden', no output is
259 259 shown.
260 260 """
261 261 self.execute('execfile("%s")' % path, hidden=hidden)
262 262
263 263 #---------------------------------------------------------------------------
264 264 # 'FrontendWidget' protected interface
265 265 #---------------------------------------------------------------------------
266 266
267 267 def _call_tip(self):
268 268 """ Shows a call tip, if appropriate, at the current cursor location.
269 269 """
270 270 # Decide if it makes sense to show a call tip
271 271 cursor = self._get_cursor()
272 272 cursor.movePosition(QtGui.QTextCursor.Left)
273 273 document = self._control.document()
274 274 if document.characterAt(cursor.position()).toAscii() != '(':
275 275 return False
276 276 context = self._get_context(cursor)
277 277 if not context:
278 278 return False
279 279
280 280 # Send the metadata request to the kernel
281 281 name = '.'.join(context)
282 282 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
283 283 self._call_tip_pos = self._get_cursor().position()
284 284 return True
285 285
286 286 def _complete(self):
287 287 """ Performs completion at the current cursor location.
288 288 """
289 289 # Decide if it makes sense to do completion
290 290 context = self._get_context()
291 291 if not context:
292 292 return False
293 293
294 294 # Send the completion request to the kernel
295 295 self._complete_id = self.kernel_manager.xreq_channel.complete(
296 296 '.'.join(context), # text
297 297 self._get_input_buffer_cursor_line(), # line
298 298 self._get_input_buffer_cursor_column(), # cursor_pos
299 299 self.input_buffer) # block
300 300 self._complete_pos = self._get_cursor().position()
301 301 return True
302 302
303 303 def _get_banner(self):
304 304 """ Gets a banner to display at the beginning of a session.
305 305 """
306 306 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
307 307 '"license" for more information.'
308 308 return banner % (sys.version, sys.platform)
309 309
310 310 def _get_context(self, cursor=None):
311 311 """ Gets the context at the current cursor location.
312 312 """
313 313 if cursor is None:
314 314 cursor = self._get_cursor()
315 315 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
316 316 QtGui.QTextCursor.KeepAnchor)
317 317 text = str(cursor.selection().toPlainText())
318 318 return self._completion_lexer.get_context(text)
319 319
320 320 def _interrupt_kernel(self):
321 321 """ Attempts to the interrupt the kernel.
322 322 """
323 323 if self.kernel_manager.has_kernel:
324 324 self.kernel_manager.signal_kernel(signal.SIGINT)
325 325 else:
326 326 self._append_plain_text('Kernel process is either remote or '
327 327 'unspecified. Cannot interrupt.\n')
328 328
329 329 def _process_execute_abort(self, msg):
330 330 """ Process a reply for an aborted execution request.
331 331 """
332 332 self._append_plain_text("ERROR: execution aborted\n")
333 333
334 334 def _process_execute_error(self, msg):
335 335 """ Process a reply for an execution request that resulted in an error.
336 336 """
337 337 content = msg['content']
338 338 traceback = ''.join(content['traceback'])
339 339 self._append_plain_text(traceback)
340 340
341 341 def _process_execute_ok(self, msg):
342 342 """ Process a reply for a successful execution equest.
343 343 """
344 344 payload = msg['content']['payload']
345 345 for item in payload:
346 346 if not self._process_execute_payload(item):
347 347 warning = 'Received unknown payload of type %s\n'
348 348 self._append_plain_text(warning % repr(item['source']))
349 349
350 350 def _process_execute_payload(self, item):
351 351 """ Process a single payload item from the list of payload items in an
352 352 execution reply. Returns whether the payload was handled.
353 353 """
354 354 # The basic FrontendWidget doesn't handle payloads, as they are a
355 355 # mechanism for going beyond the standard Python interpreter model.
356 356 return False
357 357
358 358 def _show_interpreter_prompt(self):
359 359 """ Shows a prompt for the interpreter.
360 360 """
361 361 self._show_prompt('>>> ')
362 362
363 363 def _show_interpreter_prompt_for_reply(self, msg):
364 364 """ Shows a prompt for the interpreter given an 'execute_reply' message.
365 365 """
366 366 self._show_interpreter_prompt()
367 367
368 368 #------ Signal handlers ----------------------------------------------------
369 369
370 370 def _document_contents_change(self, position, removed, added):
371 371 """ Called whenever the document's content changes. Display a call tip
372 372 if appropriate.
373 373 """
374 374 # Calculate where the cursor should be *after* the change:
375 375 position += added
376 376
377 377 document = self._control.document()
378 378 if position == self._get_cursor().position():
379 379 self._call_tip()
@@ -1,299 +1,323 b''
1 1 # Standard library imports
2 2 from subprocess import Popen
3 3
4 4 # System library imports
5 5 from PyQt4 import QtCore, QtGui
6 6
7 7 # Local imports
8 8 from IPython.core.inputsplitter import IPythonInputSplitter
9 9 from IPython.core.usage import default_banner
10 10 from frontend_widget import FrontendWidget
11 11
12 12
13 13 class IPythonPromptBlock(object):
14 14 """ An internal storage object for IPythonWidget.
15 15 """
16 16 def __init__(self, block, length, number):
17 17 self.block = block
18 18 self.length = length
19 19 self.number = number
20 20
21 21
22 22 class IPythonWidget(FrontendWidget):
23 23 """ A FrontendWidget for an IPython kernel.
24 24 """
25 25
26 26 # Signal emitted when an editor is needed for a file and the editor has been
27 27 # specified as 'custom'. See 'set_editor' for more information.
28 28 custom_edit_requested = QtCore.pyqtSignal(object, object)
29 29
30 30 # The default stylesheet: black text on a white background.
31 31 default_stylesheet = """
32 32 .error { color: red; }
33 33 .in-prompt { color: navy; }
34 34 .in-prompt-number { font-weight: bold; }
35 35 .out-prompt { color: darkred; }
36 36 .out-prompt-number { font-weight: bold; }
37 37 """
38 38
39 39 # A dark stylesheet: white text on a black background.
40 40 dark_stylesheet = """
41 41 QPlainTextEdit, QTextEdit { background-color: black; color: white }
42 42 QFrame { border: 1px solid grey; }
43 43 .error { color: red; }
44 44 .in-prompt { color: lime; }
45 45 .in-prompt-number { color: lime; font-weight: bold; }
46 46 .out-prompt { color: red; }
47 47 .out-prompt-number { color: red; font-weight: bold; }
48 48 """
49 49
50 50 # Default prompts.
51 51 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
52 52 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
53 53
54 54 # FrontendWidget protected class variables.
55 55 #_input_splitter_class = IPythonInputSplitter
56 56
57 57 # IPythonWidget protected class variables.
58 58 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
59 59 _payload_source_page = 'IPython.zmq.page.page'
60 60
61 61 #---------------------------------------------------------------------------
62 62 # 'object' interface
63 63 #---------------------------------------------------------------------------
64 64
65 65 def __init__(self, *args, **kw):
66 66 super(IPythonWidget, self).__init__(*args, **kw)
67 67
68 68 # IPythonWidget protected variables.
69 69 self._previous_prompt_obj = None
70 70
71 71 # Set a default editor and stylesheet.
72 72 self.set_editor('default')
73 73 self.reset_styling()
74 74
75 75 #---------------------------------------------------------------------------
76 76 # 'BaseFrontendMixin' abstract interface
77 77 #---------------------------------------------------------------------------
78 78
79 def _handle_history_reply(self, msg):
80 """ Implemented to handle history replies, which are only supported by
81 the IPython kernel.
82 """
83 history_dict = msg['content']['history']
84 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
85 self._set_history(items)
86
87 def _handle_prompt_reply(self, msg):
88 """ Implemented to handle prompt number replies, which are only
89 supported by the IPython kernel.
90 """
91 content = msg['content']
92 self._show_interpreter_prompt(content['prompt_number'],
93 content['input_sep'])
94
79 95 def _handle_pyout(self, msg):
80 96 """ Reimplemented for IPython-style "display hook".
81 97 """
82 98 if not self._hidden and self._is_from_this_session(msg):
83 99 content = msg['content']
84 100 prompt_number = content['prompt_number']
85 101 self._append_plain_text(content['output_sep'])
86 102 self._append_html(self._make_out_prompt(prompt_number))
87 103 self._append_plain_text(content['data'] + '\n' +
88 104 content['output_sep2'])
89 105
106 def _started_channels(self):
107 """ Reimplemented to make a history request.
108 """
109 super(IPythonWidget, self)._started_channels()
110 # FIXME: Disabled until history requests are properly implemented.
111 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
112
90 113 #---------------------------------------------------------------------------
91 114 # 'FrontendWidget' interface
92 115 #---------------------------------------------------------------------------
93 116
94 117 def execute_file(self, path, hidden=False):
95 118 """ Reimplemented to use the 'run' magic.
96 119 """
97 120 self.execute('%%run %s' % path, hidden=hidden)
98 121
99 122 #---------------------------------------------------------------------------
100 123 # 'FrontendWidget' protected interface
101 124 #---------------------------------------------------------------------------
102 125
103 126 def _get_banner(self):
104 127 """ Reimplemented to return IPython's default banner.
105 128 """
106 129 return default_banner + '\n'
107 130
108 131 def _process_execute_error(self, msg):
109 132 """ Reimplemented for IPython-style traceback formatting.
110 133 """
111 134 content = msg['content']
112 135 traceback = '\n'.join(content['traceback']) + '\n'
113 136 if False:
114 137 # FIXME: For now, tracebacks come as plain text, so we can't use
115 138 # the html renderer yet. Once we refactor ultratb to produce
116 139 # properly styled tracebacks, this branch should be the default
117 140 traceback = traceback.replace(' ', '&nbsp;')
118 141 traceback = traceback.replace('\n', '<br/>')
119 142
120 143 ename = content['ename']
121 144 ename_styled = '<span class="error">%s</span>' % ename
122 145 traceback = traceback.replace(ename, ename_styled)
123 146
124 147 self._append_html(traceback)
125 148 else:
126 149 # This is the fallback for now, using plain text with ansi escapes
127 150 self._append_plain_text(traceback)
128 151
129 152 def _process_execute_payload(self, item):
130 153 """ Reimplemented to handle %edit and paging payloads.
131 154 """
132 155 if item['source'] == self._payload_source_edit:
133 156 self.edit(item['filename'], item['line_number'])
134 157 return True
135 158 elif item['source'] == self._payload_source_page:
136 159 self._page(item['data'])
137 160 return True
138 161 else:
139 162 return False
140 163
141 164 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
142 165 """ Reimplemented for IPython-style prompts.
143 166 """
144 # TODO: If a number was not specified, make a prompt number request.
167 # If a number was not specified, make a prompt number request.
145 168 if number is None:
146 number = 0
169 self.kernel_manager.xreq_channel.prompt()
170 return
147 171
148 172 # Show a new prompt and save information about it so that it can be
149 173 # updated later if the prompt number turns out to be wrong.
150 174 self._append_plain_text(input_sep)
151 175 self._show_prompt(self._make_in_prompt(number), html=True)
152 176 block = self._control.document().lastBlock()
153 177 length = len(self._prompt)
154 178 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
155 179
156 180 # Update continuation prompt to reflect (possibly) new prompt length.
157 181 self._set_continuation_prompt(
158 182 self._make_continuation_prompt(self._prompt), html=True)
159 183
160 184 def _show_interpreter_prompt_for_reply(self, msg):
161 185 """ Reimplemented for IPython-style prompts.
162 186 """
163 187 # Update the old prompt number if necessary.
164 188 content = msg['content']
165 189 previous_prompt_number = content['prompt_number']
166 190 if self._previous_prompt_obj and \
167 191 self._previous_prompt_obj.number != previous_prompt_number:
168 192 block = self._previous_prompt_obj.block
169 193 if block.isValid():
170 194
171 195 # Remove the old prompt and insert a new prompt.
172 196 cursor = QtGui.QTextCursor(block)
173 197 cursor.movePosition(QtGui.QTextCursor.Right,
174 198 QtGui.QTextCursor.KeepAnchor,
175 199 self._previous_prompt_obj.length)
176 200 prompt = self._make_in_prompt(previous_prompt_number)
177 201 self._prompt = self._insert_html_fetching_plain_text(
178 202 cursor, prompt)
179 203
180 204 # When the HTML is inserted, Qt blows away the syntax
181 205 # highlighting for the line, so we need to rehighlight it.
182 206 self._highlighter.rehighlightBlock(cursor.block())
183 207
184 208 self._previous_prompt_obj = None
185 209
186 210 # Show a new prompt with the kernel's estimated prompt number.
187 211 next_prompt = content['next_prompt']
188 212 self._show_interpreter_prompt(next_prompt['prompt_number'],
189 213 next_prompt['input_sep'])
190 214
191 215 #---------------------------------------------------------------------------
192 216 # 'IPythonWidget' interface
193 217 #---------------------------------------------------------------------------
194 218
195 219 def edit(self, filename, line=None):
196 220 """ Opens a Python script for editing.
197 221
198 222 Parameters:
199 223 -----------
200 224 filename : str
201 225 A path to a local system file.
202 226
203 227 line : int, optional
204 228 A line of interest in the file.
205 229
206 230 Raises:
207 231 -------
208 232 OSError
209 233 If the editor command cannot be executed.
210 234 """
211 235 if self._editor == 'default':
212 236 url = QtCore.QUrl.fromLocalFile(filename)
213 237 if not QtGui.QDesktopServices.openUrl(url):
214 238 message = 'Failed to open %s with the default application'
215 239 raise OSError(message % repr(filename))
216 240 elif self._editor is None:
217 241 self.custom_edit_requested.emit(filename, line)
218 242 else:
219 243 Popen(self._editor + [filename])
220 244
221 245 def reset_styling(self):
222 246 """ Restores the default IPythonWidget styling.
223 247 """
224 248 self.set_styling(self.default_stylesheet, syntax_style='default')
225 249 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
226 250
227 251 def set_editor(self, editor):
228 252 """ Sets the editor to use with the %edit magic.
229 253
230 254 Parameters:
231 255 -----------
232 256 editor : str or sequence of str
233 257 A command suitable for use with Popen. This command will be executed
234 258 with a single argument--a filename--when editing is requested.
235 259
236 260 This parameter also takes two special values:
237 261 'default' : Files will be edited with the system default
238 262 application for Python files.
239 263 'custom' : Emit a 'custom_edit_requested(str, int)' signal
240 264 instead of opening an editor.
241 265 """
242 266 if editor == 'default':
243 267 self._editor = 'default'
244 268 elif editor == 'custom':
245 269 self._editor = None
246 270 elif isinstance(editor, basestring):
247 271 self._editor = [ editor ]
248 272 else:
249 273 self._editor = list(editor)
250 274
251 275 def set_styling(self, stylesheet, syntax_style=None):
252 276 """ Sets the IPythonWidget styling.
253 277
254 278 Parameters:
255 279 -----------
256 280 stylesheet : str
257 281 A CSS stylesheet. The stylesheet can contain classes for:
258 282 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
259 283 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
260 284 3. IPython: .error, .in-prompt, .out-prompt, etc.
261 285
262 286 syntax_style : str or None [default None]
263 287 If specified, use the Pygments style with given name. Otherwise,
264 288 the stylesheet is queried for Pygments style information.
265 289 """
266 290 self.setStyleSheet(stylesheet)
267 291 self._control.document().setDefaultStyleSheet(stylesheet)
268 292 if self._page_control:
269 293 self._page_control.document().setDefaultStyleSheet(stylesheet)
270 294
271 295 if syntax_style is None:
272 296 self._highlighter.set_style_sheet(stylesheet)
273 297 else:
274 298 self._highlighter.set_style(syntax_style)
275 299
276 300 #---------------------------------------------------------------------------
277 301 # 'IPythonWidget' protected interface
278 302 #---------------------------------------------------------------------------
279 303
280 304 def _make_in_prompt(self, number):
281 305 """ Given a prompt number, returns an HTML In prompt.
282 306 """
283 307 body = self.in_prompt % number
284 308 return '<span class="in-prompt">%s</span>' % body
285 309
286 310 def _make_continuation_prompt(self, prompt):
287 311 """ Given a plain text version of an In prompt, returns an HTML
288 312 continuation prompt.
289 313 """
290 314 end_chars = '...: '
291 315 space_count = len(prompt.lstrip('\n')) - len(end_chars)
292 316 body = '&nbsp;' * space_count + end_chars
293 317 return '<span class="in-prompt">%s</span>' % body
294 318
295 319 def _make_out_prompt(self, number):
296 320 """ Given a prompt number, returns an HTML Out prompt.
297 321 """
298 322 body = self.out_prompt % number
299 323 return '<span class="out-prompt">%s</span>' % body
@@ -1,398 +1,399 b''
1 1 #!/usr/bin/env python
2 2 """A simple interactive kernel that talks to a frontend over 0MQ.
3 3
4 4 Things to do:
5 5
6 6 * Implement `set_parent` logic. Right before doing exec, the Kernel should
7 7 call set_parent on all the PUB objects with the message about to be executed.
8 8 * Implement random port and security key logic.
9 9 * Implement control messages.
10 10 * Implement event loop and poll version.
11 11 """
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 # Standard library imports.
18 18 import __builtin__
19 19 import sys
20 20 import time
21 21 import traceback
22 22
23 23 # System library imports.
24 24 import zmq
25 25
26 26 # Local imports.
27 27 from IPython.config.configurable import Configurable
28 28 from IPython.utils.traitlets import Instance
29 29 from completer import KernelCompleter
30 30 from entry_point import base_launch_kernel, make_argument_parser, make_kernel, \
31 31 start_kernel
32 32 from iostream import OutStream
33 33 from session import Session, Message
34 34 from zmqshell import ZMQInteractiveShell
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Main kernel class
38 38 #-----------------------------------------------------------------------------
39 39
40 40 class Kernel(Configurable):
41 41
42 42 #---------------------------------------------------------------------------
43 43 # Kernel interface
44 44 #---------------------------------------------------------------------------
45 45
46 46 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
47 47 session = Instance(Session)
48 48 reply_socket = Instance('zmq.Socket')
49 49 pub_socket = Instance('zmq.Socket')
50 50 req_socket = Instance('zmq.Socket')
51 51
52 52 # Maps user-friendly backend names to matplotlib backend identifiers.
53 53 _pylab_map = { 'tk': 'TkAgg',
54 54 'gtk': 'GTKAgg',
55 55 'wx': 'WXAgg',
56 56 'qt': 'Qt4Agg', # qt3 not supported
57 57 'qt4': 'Qt4Agg',
58 58 'payload-svg' : \
59 59 'module://IPython.zmq.pylab.backend_payload_svg' }
60 60
61 61 def __init__(self, **kwargs):
62 62 super(Kernel, self).__init__(**kwargs)
63 63
64 64 # Initialize the InteractiveShell subclass
65 65 self.shell = ZMQInteractiveShell.instance()
66 66 self.shell.displayhook.session = self.session
67 67 self.shell.displayhook.pub_socket = self.pub_socket
68 68
69 69 # TMP - hack while developing
70 70 self.shell._reply_content = None
71 71
72 72 # Build dict of handlers for message types
73 73 msg_types = [ 'execute_request', 'complete_request',
74 74 'object_info_request', 'prompt_request',
75 75 'history_request' ]
76 76 self.handlers = {}
77 77 for msg_type in msg_types:
78 78 self.handlers[msg_type] = getattr(self, msg_type)
79 79
80 80 def activate_pylab(self, backend=None, import_all=True):
81 81 """ Activates pylab in this kernel's namespace.
82 82
83 83 Parameters:
84 84 -----------
85 85 backend : str, optional
86 86 A valid backend name.
87 87
88 88 import_all : bool, optional
89 89 If true, an 'import *' is done from numpy and pylab.
90 90 """
91 91 # FIXME: This is adapted from IPython.lib.pylabtools.pylab_activate.
92 92 # Common functionality should be refactored.
93 93
94 94 # We must set the desired backend before importing pylab.
95 95 import matplotlib
96 96 if backend:
97 97 backend_id = self._pylab_map[backend]
98 98 if backend_id.startswith('module://'):
99 99 # Work around bug in matplotlib: matplotlib.use converts the
100 100 # backend_id to lowercase even if a module name is specified!
101 101 matplotlib.rcParams['backend'] = backend_id
102 102 else:
103 103 matplotlib.use(backend_id)
104 104
105 105 # Import numpy as np/pyplot as plt are conventions we're trying to
106 106 # somewhat standardize on. Making them available to users by default
107 107 # will greatly help this.
108 108 exec ("import numpy\n"
109 109 "import matplotlib\n"
110 110 "from matplotlib import pylab, mlab, pyplot\n"
111 111 "np = numpy\n"
112 112 "plt = pyplot\n"
113 113 ) in self.shell.user_ns
114 114
115 115 if import_all:
116 116 exec("from matplotlib.pylab import *\n"
117 117 "from numpy import *\n") in self.shell.user_ns
118 118
119 119 matplotlib.interactive(True)
120 120
121 121 def start(self):
122 122 """ Start the kernel main loop.
123 123 """
124 124 while True:
125 125 ident = self.reply_socket.recv()
126 126 assert self.reply_socket.rcvmore(), "Missing message part."
127 127 msg = self.reply_socket.recv_json()
128 128 omsg = Message(msg)
129 129 print>>sys.__stdout__
130 130 print>>sys.__stdout__, omsg
131 131 handler = self.handlers.get(omsg.msg_type, None)
132 132 if handler is None:
133 133 print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg
134 134 else:
135 135 handler(ident, omsg)
136 136
137 137 #---------------------------------------------------------------------------
138 138 # Kernel request handlers
139 139 #---------------------------------------------------------------------------
140 140
141 141 def execute_request(self, ident, parent):
142 142 try:
143 143 code = parent[u'content'][u'code']
144 144 except:
145 145 print>>sys.__stderr__, "Got bad msg: "
146 146 print>>sys.__stderr__, Message(parent)
147 147 return
148 148 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
149 149 self.pub_socket.send_json(pyin_msg)
150 150
151 151 try:
152 152 # Replace raw_input. Note that is not sufficient to replace
153 153 # raw_input in the user namespace.
154 154 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
155 155 __builtin__.raw_input = raw_input
156 156
157 157 # Set the parent message of the display hook and out streams.
158 158 self.shell.displayhook.set_parent(parent)
159 159 sys.stdout.set_parent(parent)
160 160 sys.stderr.set_parent(parent)
161 161
162 162 # FIXME: runlines calls the exception handler itself. We should
163 163 # clean this up.
164 164 self.shell._reply_content = None
165 165 self.shell.runlines(code)
166 166 except:
167 167 # FIXME: this code right now isn't being used yet by default,
168 168 # because the runlines() call above directly fires off exception
169 169 # reporting. This code, therefore, is only active in the scenario
170 170 # where runlines itself has an unhandled exception. We need to
171 171 # uniformize this, for all exception construction to come from a
172 172 # single location in the codbase.
173 173 etype, evalue, tb = sys.exc_info()
174 174 tb_list = traceback.format_exception(etype, evalue, tb)
175 175 reply_content = self.shell._showtraceback(etype, evalue, tb_list)
176 176 else:
177 177 payload = self.shell.payload_manager.read_payload()
178 178 # Be agressive about clearing the payload because we don't want
179 179 # it to sit in memory until the next execute_request comes in.
180 180 self.shell.payload_manager.clear_payload()
181 181 reply_content = { 'status' : 'ok', 'payload' : payload }
182 182
183 183 # Compute the prompt information
184 184 prompt_number = self.shell.displayhook.prompt_count
185 185 reply_content['prompt_number'] = prompt_number
186 186 prompt_string = self.shell.displayhook.prompt1.peek_next_prompt()
187 187 next_prompt = {'prompt_string' : prompt_string,
188 188 'prompt_number' : prompt_number+1,
189 189 'input_sep' : self.shell.displayhook.input_sep}
190 190 reply_content['next_prompt'] = next_prompt
191 191
192 192 # TMP - fish exception info out of shell, possibly left there by
193 193 # runlines
194 194 if self.shell._reply_content is not None:
195 195 reply_content.update(self.shell._reply_content)
196 196
197 197 # Flush output before sending the reply.
198 198 sys.stderr.flush()
199 199 sys.stdout.flush()
200 200
201 201 # Send the reply.
202 202 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
203 203 print>>sys.__stdout__, Message(reply_msg)
204 204 self.reply_socket.send(ident, zmq.SNDMORE)
205 205 self.reply_socket.send_json(reply_msg)
206 206 if reply_msg['content']['status'] == u'error':
207 207 self._abort_queue()
208 208
209 209 def complete_request(self, ident, parent):
210 210 matches = {'matches' : self._complete(parent),
211 211 'status' : 'ok'}
212 212 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
213 213 matches, parent, ident)
214 214 print >> sys.__stdout__, completion_msg
215 215
216 216 def object_info_request(self, ident, parent):
217 217 context = parent['content']['oname'].split('.')
218 218 object_info = self._object_info(context)
219 219 msg = self.session.send(self.reply_socket, 'object_info_reply',
220 220 object_info, parent, ident)
221 221 print >> sys.__stdout__, msg
222 222
223 223 def prompt_request(self, ident, parent):
224 224 prompt_number = self.shell.displayhook.prompt_count
225 225 prompt_string = self.shell.displayhook.prompt1.peek_next_prompt()
226 226 content = {'prompt_string' : prompt_string,
227 'prompt_number' : prompt_number+1}
227 'prompt_number' : prompt_number+1,
228 'input_sep' : self.shell.displayhook.input_sep}
228 229 msg = self.session.send(self.reply_socket, 'prompt_reply',
229 230 content, parent, ident)
230 231 print >> sys.__stdout__, msg
231 232
232 233 def history_request(self, ident, parent):
233 output = parent['content'].get('output', True)
234 index = parent['content'].get('index')
235 raw = parent['content'].get('raw', False)
234 output = parent['content']['output']
235 index = parent['content']['index']
236 raw = parent['content']['raw']
236 237 hist = self.shell.get_history(index=index, raw=raw, output=output)
237 238 content = {'history' : hist}
238 239 msg = self.session.send(self.reply_socket, 'history_reply',
239 240 content, parent, ident)
240 241 print >> sys.__stdout__, msg
241 242
242 243 #---------------------------------------------------------------------------
243 244 # Protected interface
244 245 #---------------------------------------------------------------------------
245 246
246 247 def _abort_queue(self):
247 248 while True:
248 249 try:
249 250 ident = self.reply_socket.recv(zmq.NOBLOCK)
250 251 except zmq.ZMQError, e:
251 252 if e.errno == zmq.EAGAIN:
252 253 break
253 254 else:
254 255 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
255 256 msg = self.reply_socket.recv_json()
256 257 print>>sys.__stdout__, "Aborting:"
257 258 print>>sys.__stdout__, Message(msg)
258 259 msg_type = msg['msg_type']
259 260 reply_type = msg_type.split('_')[0] + '_reply'
260 261 reply_msg = self.session.msg(reply_type, {'status' : 'aborted'}, msg)
261 262 print>>sys.__stdout__, Message(reply_msg)
262 263 self.reply_socket.send(ident,zmq.SNDMORE)
263 264 self.reply_socket.send_json(reply_msg)
264 265 # We need to wait a bit for requests to come in. This can probably
265 266 # be set shorter for true asynchronous clients.
266 267 time.sleep(0.1)
267 268
268 269 def _raw_input(self, prompt, ident, parent):
269 270 # Flush output before making the request.
270 271 sys.stderr.flush()
271 272 sys.stdout.flush()
272 273
273 274 # Send the input request.
274 275 content = dict(prompt=prompt)
275 276 msg = self.session.msg(u'input_request', content, parent)
276 277 self.req_socket.send_json(msg)
277 278
278 279 # Await a response.
279 280 reply = self.req_socket.recv_json()
280 281 try:
281 282 value = reply['content']['value']
282 283 except:
283 284 print>>sys.__stderr__, "Got bad raw_input reply: "
284 285 print>>sys.__stderr__, Message(parent)
285 286 value = ''
286 287 return value
287 288
288 289 def _complete(self, msg):
289 290 #from IPython.utils.io import rprint # dbg
290 291 #rprint('\n\n**MSG**\n\n', msg) # dbg
291 292 #import traceback; rprint(''.join(traceback.format_stack())) # dbg
292 293 c = msg['content']
293 294 try:
294 295 cpos = int(c['cursor_pos'])
295 296 except:
296 297 # If we don't get something that we can convert to an integer, at
297 298 # leasat attempt the completion guessing the cursor is at the end
298 299 # of the text
299 300 cpos = len(c['text'])
300 301 return self.shell.complete(c['text'], c['line'], cpos)
301 302
302 303 def _object_info(self, context):
303 304 symbol, leftover = self._symbol_from_context(context)
304 305 if symbol is not None and not leftover:
305 306 doc = getattr(symbol, '__doc__', '')
306 307 else:
307 308 doc = ''
308 309 object_info = dict(docstring = doc)
309 310 return object_info
310 311
311 312 def _symbol_from_context(self, context):
312 313 if not context:
313 314 return None, context
314 315
315 316 base_symbol_string = context[0]
316 317 symbol = self.shell.user_ns.get(base_symbol_string, None)
317 318 if symbol is None:
318 319 symbol = __builtin__.__dict__.get(base_symbol_string, None)
319 320 if symbol is None:
320 321 return None, context
321 322
322 323 context = context[1:]
323 324 for i, name in enumerate(context):
324 325 new_symbol = getattr(symbol, name, None)
325 326 if new_symbol is None:
326 327 return symbol, context[i:]
327 328 else:
328 329 symbol = new_symbol
329 330
330 331 return symbol, []
331 332
332 333 #-----------------------------------------------------------------------------
333 334 # Kernel main and launch functions
334 335 #-----------------------------------------------------------------------------
335 336
336 337 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, independent=False,
337 338 pylab=False):
338 339 """ Launches a localhost kernel, binding to the specified ports.
339 340
340 341 Parameters
341 342 ----------
342 343 xrep_port : int, optional
343 344 The port to use for XREP channel.
344 345
345 346 pub_port : int, optional
346 347 The port to use for the SUB channel.
347 348
348 349 req_port : int, optional
349 350 The port to use for the REQ (raw input) channel.
350 351
351 352 independent : bool, optional (default False)
352 353 If set, the kernel process is guaranteed to survive if this process
353 354 dies. If not set, an effort is made to ensure that the kernel is killed
354 355 when this process dies. Note that in this case it is still good practice
355 356 to kill kernels manually before exiting.
356 357
357 358 pylab : bool or string, optional (default False)
358 359 If not False, the kernel will be launched with pylab enabled. If a
359 360 string is passed, matplotlib will use the specified backend. Otherwise,
360 361 matplotlib's default backend will be used.
361 362
362 363 Returns
363 364 -------
364 365 A tuple of form:
365 366 (kernel_process, xrep_port, pub_port, req_port)
366 367 where kernel_process is a Popen object and the ports are integers.
367 368 """
368 369 extra_arguments = []
369 370 if pylab:
370 371 extra_arguments.append('--pylab')
371 372 if isinstance(pylab, basestring):
372 373 extra_arguments.append(pylab)
373 374 return base_launch_kernel('from IPython.zmq.ipkernel import main; main()',
374 375 xrep_port, pub_port, req_port, independent,
375 376 extra_arguments)
376 377
377 378 def main():
378 379 """ The IPython kernel main entry point.
379 380 """
380 381 parser = make_argument_parser()
381 382 parser.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
382 383 const='auto', help = \
383 384 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
384 385 given, the GUI backend is matplotlib's, otherwise use one of: \
385 386 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
386 387 namespace = parser.parse_args()
387 388
388 389 kernel = make_kernel(namespace, Kernel, OutStream)
389 390 if namespace.pylab:
390 391 if namespace.pylab == 'auto':
391 392 kernel.activate_pylab()
392 393 else:
393 394 kernel.activate_pylab(namespace.pylab)
394 395
395 396 start_kernel(namespace, kernel)
396 397
397 398 if __name__ == '__main__':
398 399 main()
@@ -1,581 +1,617 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.traitlets import HasTraits, Any, Instance, Type, TCPAddress
33 33 from session import Session
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Constants and exceptions
37 37 #-----------------------------------------------------------------------------
38 38
39 39 LOCALHOST = '127.0.0.1'
40 40
41 41 class InvalidPortNumber(Exception):
42 42 pass
43 43
44 44 #-----------------------------------------------------------------------------
45 45 # ZMQ Socket Channel classes
46 46 #-----------------------------------------------------------------------------
47 47
48 48 class ZmqSocketChannel(Thread):
49 49 """The base class for the channels that use ZMQ sockets.
50 50 """
51 51 context = None
52 52 session = None
53 53 socket = None
54 54 ioloop = None
55 55 iostate = None
56 56 _address = None
57 57
58 58 def __init__(self, context, session, address):
59 59 """Create a channel
60 60
61 61 Parameters
62 62 ----------
63 63 context : :class:`zmq.Context`
64 64 The ZMQ context to use.
65 65 session : :class:`session.Session`
66 66 The session to use.
67 67 address : tuple
68 68 Standard (ip, port) tuple that the kernel is listening on.
69 69 """
70 70 super(ZmqSocketChannel, self).__init__()
71 71 self.daemon = True
72 72
73 73 self.context = context
74 74 self.session = session
75 75 if address[1] == 0:
76 76 message = 'The port number for a channel cannot be 0.'
77 77 raise InvalidPortNumber(message)
78 78 self._address = address
79 79
80 80 def stop(self):
81 81 """Stop the channel's activity.
82 82
83 83 This calls :method:`Thread.join` and returns when the thread
84 84 terminates. :class:`RuntimeError` will be raised if
85 85 :method:`self.start` is called again.
86 86 """
87 87 self.join()
88 88
89 89 @property
90 90 def address(self):
91 91 """Get the channel's address as an (ip, port) tuple.
92 92
93 93 By the default, the address is (localhost, 0), where 0 means a random
94 94 port.
95 95 """
96 96 return self._address
97 97
98 98 def add_io_state(self, state):
99 99 """Add IO state to the eventloop.
100 100
101 101 Parameters
102 102 ----------
103 103 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
104 104 The IO state flag to set.
105 105
106 106 This is thread safe as it uses the thread safe IOLoop.add_callback.
107 107 """
108 108 def add_io_state_callback():
109 109 if not self.iostate & state:
110 110 self.iostate = self.iostate | state
111 111 self.ioloop.update_handler(self.socket, self.iostate)
112 112 self.ioloop.add_callback(add_io_state_callback)
113 113
114 114 def drop_io_state(self, state):
115 115 """Drop IO state from the eventloop.
116 116
117 117 Parameters
118 118 ----------
119 119 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
120 120 The IO state flag to set.
121 121
122 122 This is thread safe as it uses the thread safe IOLoop.add_callback.
123 123 """
124 124 def drop_io_state_callback():
125 125 if self.iostate & state:
126 126 self.iostate = self.iostate & (~state)
127 127 self.ioloop.update_handler(self.socket, self.iostate)
128 128 self.ioloop.add_callback(drop_io_state_callback)
129 129
130 130
131 131 class XReqSocketChannel(ZmqSocketChannel):
132 132 """The XREQ channel for issues request/replies to the kernel.
133 133 """
134 134
135 135 command_queue = None
136 136
137 137 def __init__(self, context, session, address):
138 138 self.command_queue = Queue()
139 139 super(XReqSocketChannel, self).__init__(context, session, address)
140 140
141 141 def run(self):
142 142 """The thread's main activity. Call start() instead."""
143 143 self.socket = self.context.socket(zmq.XREQ)
144 144 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
145 145 self.socket.connect('tcp://%s:%i' % self.address)
146 146 self.ioloop = ioloop.IOLoop()
147 147 self.iostate = POLLERR|POLLIN
148 148 self.ioloop.add_handler(self.socket, self._handle_events,
149 149 self.iostate)
150 150 self.ioloop.start()
151 151
152 152 def stop(self):
153 153 self.ioloop.stop()
154 154 super(XReqSocketChannel, self).stop()
155 155
156 156 def call_handlers(self, msg):
157 157 """This method is called in the ioloop thread when a message arrives.
158 158
159 159 Subclasses should override this method to handle incoming messages.
160 160 It is important to remember that this method is called in the thread
161 161 so that some logic must be done to ensure that the application leve
162 162 handlers are called in the application thread.
163 163 """
164 164 raise NotImplementedError('call_handlers must be defined in a subclass.')
165 165
166 def execute(self, code):
166 def execute(self, code, silent=False):
167 167 """Execute code in the kernel.
168 168
169 169 Parameters
170 170 ----------
171 171 code : str
172 172 A string of Python code.
173 silent : bool, optional (default False)
174 If set, the kernel will execute the code as quietly possible.
173 175
174 176 Returns
175 177 -------
176 178 The msg_id of the message sent.
177 179 """
178 180 # Create class for content/msg creation. Related to, but possibly
179 181 # not in Session.
180 content = dict(code=code)
182 content = dict(code=code, silent=silent)
181 183 msg = self.session.msg('execute_request', content)
182 184 self._queue_request(msg)
183 185 return msg['header']['msg_id']
184 186
185 187 def complete(self, text, line, cursor_pos, block=None):
186 188 """Tab complete text in the kernel's namespace.
187 189
188 190 Parameters
189 191 ----------
190 192 text : str
191 193 The text to complete.
192 194 line : str
193 195 The full line of text that is the surrounding context for the
194 196 text to complete.
195 197 cursor_pos : int
196 198 The position of the cursor in the line where the completion was
197 199 requested.
198 200 block : str, optional
199 201 The full block of code in which the completion is being requested.
200 202
201 203 Returns
202 204 -------
203 205 The msg_id of the message sent.
204 206 """
205 207 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
206 208 msg = self.session.msg('complete_request', content)
207 209 self._queue_request(msg)
208 210 return msg['header']['msg_id']
209 211
210 212 def object_info(self, oname):
211 213 """Get metadata information about an object.
212 214
213 215 Parameters
214 216 ----------
215 217 oname : str
216 218 A string specifying the object name.
217 219
218 220 Returns
219 221 -------
220 222 The msg_id of the message sent.
221 223 """
222 224 content = dict(oname=oname)
223 225 msg = self.session.msg('object_info_request', content)
224 226 self._queue_request(msg)
225 227 return msg['header']['msg_id']
226 228
229 def history(self, index=None, raw=False, output=True):
230 """Get the history list.
231
232 Parameters
233 ----------
234 index : n or (n1, n2) or None
235 If n, then the last entries. If a tuple, then all in
236 range(n1, n2). If None, then all entries. Raises IndexError if
237 the format of index is incorrect.
238 raw : bool
239 If True, return the raw input.
240 output : bool
241 If True, then return the output as well.
242
243 Returns
244 -------
245 The msg_id of the message sent.
246 """
247 content = dict(index=index, raw=raw, output=output)
248 msg = self.session.msg('history_request', content)
249 self._queue_request(msg)
250 return msg['header']['msg_id']
251
252 def prompt(self):
253 """Requests a prompt number from the kernel.
254
255 Returns
256 -------
257 The msg_id of the message sent.
258 """
259 msg = self.session.msg('prompt_request')
260 self._queue_request(msg)
261 return msg['header']['msg_id']
262
227 263 def _handle_events(self, socket, events):
228 264 if events & POLLERR:
229 265 self._handle_err()
230 266 if events & POLLOUT:
231 267 self._handle_send()
232 268 if events & POLLIN:
233 269 self._handle_recv()
234 270
235 271 def _handle_recv(self):
236 272 msg = self.socket.recv_json()
237 273 self.call_handlers(msg)
238 274
239 275 def _handle_send(self):
240 276 try:
241 277 msg = self.command_queue.get(False)
242 278 except Empty:
243 279 pass
244 280 else:
245 281 self.socket.send_json(msg)
246 282 if self.command_queue.empty():
247 283 self.drop_io_state(POLLOUT)
248 284
249 285 def _handle_err(self):
250 286 # We don't want to let this go silently, so eventually we should log.
251 287 raise zmq.ZMQError()
252 288
253 289 def _queue_request(self, msg):
254 290 self.command_queue.put(msg)
255 291 self.add_io_state(POLLOUT)
256 292
257 293
258 294 class SubSocketChannel(ZmqSocketChannel):
259 295 """The SUB channel which listens for messages that the kernel publishes.
260 296 """
261 297
262 298 def __init__(self, context, session, address):
263 299 super(SubSocketChannel, self).__init__(context, session, address)
264 300
265 301 def run(self):
266 302 """The thread's main activity. Call start() instead."""
267 303 self.socket = self.context.socket(zmq.SUB)
268 304 self.socket.setsockopt(zmq.SUBSCRIBE,'')
269 305 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
270 306 self.socket.connect('tcp://%s:%i' % self.address)
271 307 self.ioloop = ioloop.IOLoop()
272 308 self.iostate = POLLIN|POLLERR
273 309 self.ioloop.add_handler(self.socket, self._handle_events,
274 310 self.iostate)
275 311 self.ioloop.start()
276 312
277 313 def stop(self):
278 314 self.ioloop.stop()
279 315 super(SubSocketChannel, self).stop()
280 316
281 317 def call_handlers(self, msg):
282 318 """This method is called in the ioloop thread when a message arrives.
283 319
284 320 Subclasses should override this method to handle incoming messages.
285 321 It is important to remember that this method is called in the thread
286 322 so that some logic must be done to ensure that the application leve
287 323 handlers are called in the application thread.
288 324 """
289 325 raise NotImplementedError('call_handlers must be defined in a subclass.')
290 326
291 327 def flush(self, timeout=1.0):
292 328 """Immediately processes all pending messages on the SUB channel.
293 329
294 330 Callers should use this method to ensure that :method:`call_handlers`
295 331 has been called for all messages that have been received on the
296 332 0MQ SUB socket of this channel.
297 333
298 334 This method is thread safe.
299 335
300 336 Parameters
301 337 ----------
302 338 timeout : float, optional
303 339 The maximum amount of time to spend flushing, in seconds. The
304 340 default is one second.
305 341 """
306 342 # We do the IOLoop callback process twice to ensure that the IOLoop
307 343 # gets to perform at least one full poll.
308 344 stop_time = time.time() + timeout
309 345 for i in xrange(2):
310 346 self._flushed = False
311 347 self.ioloop.add_callback(self._flush)
312 348 while not self._flushed and time.time() < stop_time:
313 349 time.sleep(0.01)
314 350
315 351 def _handle_events(self, socket, events):
316 352 # Turn on and off POLLOUT depending on if we have made a request
317 353 if events & POLLERR:
318 354 self._handle_err()
319 355 if events & POLLIN:
320 356 self._handle_recv()
321 357
322 358 def _handle_err(self):
323 359 # We don't want to let this go silently, so eventually we should log.
324 360 raise zmq.ZMQError()
325 361
326 362 def _handle_recv(self):
327 363 # Get all of the messages we can
328 364 while True:
329 365 try:
330 366 msg = self.socket.recv_json(zmq.NOBLOCK)
331 367 except zmq.ZMQError:
332 368 # Check the errno?
333 369 # Will this trigger POLLERR?
334 370 break
335 371 else:
336 372 self.call_handlers(msg)
337 373
338 374 def _flush(self):
339 375 """Callback for :method:`self.flush`."""
340 376 self._flushed = True
341 377
342 378
343 379 class RepSocketChannel(ZmqSocketChannel):
344 380 """A reply channel to handle raw_input requests that the kernel makes."""
345 381
346 382 msg_queue = None
347 383
348 384 def __init__(self, context, session, address):
349 385 self.msg_queue = Queue()
350 386 super(RepSocketChannel, self).__init__(context, session, address)
351 387
352 388 def run(self):
353 389 """The thread's main activity. Call start() instead."""
354 390 self.socket = self.context.socket(zmq.XREQ)
355 391 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
356 392 self.socket.connect('tcp://%s:%i' % self.address)
357 393 self.ioloop = ioloop.IOLoop()
358 394 self.iostate = POLLERR|POLLIN
359 395 self.ioloop.add_handler(self.socket, self._handle_events,
360 396 self.iostate)
361 397 self.ioloop.start()
362 398
363 399 def stop(self):
364 400 self.ioloop.stop()
365 401 super(RepSocketChannel, self).stop()
366 402
367 403 def call_handlers(self, msg):
368 404 """This method is called in the ioloop thread when a message arrives.
369 405
370 406 Subclasses should override this method to handle incoming messages.
371 407 It is important to remember that this method is called in the thread
372 408 so that some logic must be done to ensure that the application leve
373 409 handlers are called in the application thread.
374 410 """
375 411 raise NotImplementedError('call_handlers must be defined in a subclass.')
376 412
377 413 def input(self, string):
378 414 """Send a string of raw input to the kernel."""
379 415 content = dict(value=string)
380 416 msg = self.session.msg('input_reply', content)
381 417 self._queue_reply(msg)
382 418
383 419 def _handle_events(self, socket, events):
384 420 if events & POLLERR:
385 421 self._handle_err()
386 422 if events & POLLOUT:
387 423 self._handle_send()
388 424 if events & POLLIN:
389 425 self._handle_recv()
390 426
391 427 def _handle_recv(self):
392 428 msg = self.socket.recv_json()
393 429 self.call_handlers(msg)
394 430
395 431 def _handle_send(self):
396 432 try:
397 433 msg = self.msg_queue.get(False)
398 434 except Empty:
399 435 pass
400 436 else:
401 437 self.socket.send_json(msg)
402 438 if self.msg_queue.empty():
403 439 self.drop_io_state(POLLOUT)
404 440
405 441 def _handle_err(self):
406 442 # We don't want to let this go silently, so eventually we should log.
407 443 raise zmq.ZMQError()
408 444
409 445 def _queue_reply(self, msg):
410 446 self.msg_queue.put(msg)
411 447 self.add_io_state(POLLOUT)
412 448
413 449
414 450 #-----------------------------------------------------------------------------
415 451 # Main kernel manager class
416 452 #-----------------------------------------------------------------------------
417 453
418 454 class KernelManager(HasTraits):
419 455 """ Manages a kernel for a frontend.
420 456
421 457 The SUB channel is for the frontend to receive messages published by the
422 458 kernel.
423 459
424 460 The REQ channel is for the frontend to make requests of the kernel.
425 461
426 462 The REP channel is for the kernel to request stdin (raw_input) from the
427 463 frontend.
428 464 """
429 465 # The PyZMQ Context to use for communication with the kernel.
430 466 context = Instance(zmq.Context,(),{})
431 467
432 468 # The Session to use for communication with the kernel.
433 469 session = Instance(Session,(),{})
434 470
435 471 # The kernel process with which the KernelManager is communicating.
436 472 kernel = Instance(Popen)
437 473
438 474 # The addresses for the communication channels.
439 475 xreq_address = TCPAddress((LOCALHOST, 0))
440 476 sub_address = TCPAddress((LOCALHOST, 0))
441 477 rep_address = TCPAddress((LOCALHOST, 0))
442 478
443 479 # The classes to use for the various channels.
444 480 xreq_channel_class = Type(XReqSocketChannel)
445 481 sub_channel_class = Type(SubSocketChannel)
446 482 rep_channel_class = Type(RepSocketChannel)
447 483
448 484 # Protected traits.
449 485 _xreq_channel = Any
450 486 _sub_channel = Any
451 487 _rep_channel = Any
452 488
453 489 #--------------------------------------------------------------------------
454 490 # Channel management methods:
455 491 #--------------------------------------------------------------------------
456 492
457 493 def start_channels(self):
458 494 """Starts the channels for this kernel.
459 495
460 496 This will create the channels if they do not exist and then start
461 497 them. If port numbers of 0 are being used (random ports) then you
462 498 must first call :method:`start_kernel`. If the channels have been
463 499 stopped and you call this, :class:`RuntimeError` will be raised.
464 500 """
465 501 self.xreq_channel.start()
466 502 self.sub_channel.start()
467 503 self.rep_channel.start()
468 504
469 505 def stop_channels(self):
470 506 """Stops the channels for this kernel.
471 507
472 508 This stops the channels by joining their threads. If the channels
473 509 were not started, :class:`RuntimeError` will be raised.
474 510 """
475 511 self.xreq_channel.stop()
476 512 self.sub_channel.stop()
477 513 self.rep_channel.stop()
478 514
479 515 @property
480 516 def channels_running(self):
481 517 """Are all of the channels created and running?"""
482 518 return self.xreq_channel.is_alive() \
483 519 and self.sub_channel.is_alive() \
484 520 and self.rep_channel.is_alive()
485 521
486 522 #--------------------------------------------------------------------------
487 523 # Kernel process management methods:
488 524 #--------------------------------------------------------------------------
489 525
490 526 def start_kernel(self, ipython=True, **kw):
491 527 """Starts a kernel process and configures the manager to use it.
492 528
493 529 If random ports (port=0) are being used, this method must be called
494 530 before the channels are created.
495 531
496 532 Parameters:
497 533 -----------
498 534 ipython : bool, optional (default True)
499 535 Whether to use an IPython kernel instead of a plain Python kernel.
500 536 """
501 537 xreq, sub, rep = self.xreq_address, self.sub_address, self.rep_address
502 538 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or rep[0] != LOCALHOST:
503 539 raise RuntimeError("Can only launch a kernel on localhost."
504 540 "Make sure that the '*_address' attributes are "
505 541 "configured properly.")
506 542
507 543 if ipython:
508 544 from ipkernel import launch_kernel as launch
509 545 else:
510 546 from pykernel import launch_kernel as launch
511 547 self.kernel, xrep, pub, req = launch(xrep_port=xreq[1], pub_port=sub[1],
512 548 req_port=rep[1], **kw)
513 549 self.xreq_address = (LOCALHOST, xrep)
514 550 self.sub_address = (LOCALHOST, pub)
515 551 self.rep_address = (LOCALHOST, req)
516 552
517 553 @property
518 554 def has_kernel(self):
519 555 """Returns whether a kernel process has been specified for the kernel
520 556 manager.
521 557 """
522 558 return self.kernel is not None
523 559
524 560 def kill_kernel(self):
525 561 """ Kill the running kernel. """
526 562 if self.kernel is not None:
527 563 self.kernel.kill()
528 564 self.kernel = None
529 565 else:
530 566 raise RuntimeError("Cannot kill kernel. No kernel is running!")
531 567
532 568 def signal_kernel(self, signum):
533 569 """ Sends a signal to the kernel. """
534 570 if self.kernel is not None:
535 571 self.kernel.send_signal(signum)
536 572 else:
537 573 raise RuntimeError("Cannot signal kernel. No kernel is running!")
538 574
539 575 @property
540 576 def is_alive(self):
541 577 """Is the kernel process still running?"""
542 578 if self.kernel is not None:
543 579 if self.kernel.poll() is None:
544 580 return True
545 581 else:
546 582 return False
547 583 else:
548 584 # We didn't start the kernel with this KernelManager so we don't
549 585 # know if it is running. We should use a heartbeat for this case.
550 586 return True
551 587
552 588 #--------------------------------------------------------------------------
553 589 # Channels used for communication with the kernel:
554 590 #--------------------------------------------------------------------------
555 591
556 592 @property
557 593 def xreq_channel(self):
558 594 """Get the REQ socket channel object to make requests of the kernel."""
559 595 if self._xreq_channel is None:
560 596 self._xreq_channel = self.xreq_channel_class(self.context,
561 597 self.session,
562 598 self.xreq_address)
563 599 return self._xreq_channel
564 600
565 601 @property
566 602 def sub_channel(self):
567 603 """Get the SUB socket channel object."""
568 604 if self._sub_channel is None:
569 605 self._sub_channel = self.sub_channel_class(self.context,
570 606 self.session,
571 607 self.sub_address)
572 608 return self._sub_channel
573 609
574 610 @property
575 611 def rep_channel(self):
576 612 """Get the REP socket channel object to handle stdin (raw_input)."""
577 613 if self._rep_channel is None:
578 614 self._rep_channel = self.rep_channel_class(self.context,
579 615 self.session,
580 616 self.rep_address)
581 617 return self._rep_channel
@@ -1,590 +1,587 b''
1 1 .. _messaging:
2 2
3 3 ======================
4 4 Messaging in IPython
5 5 ======================
6 6
7 7
8 8 Introduction
9 9 ============
10 10
11 11 This document explains the basic communications design and messaging
12 12 specification for how the various IPython objects interact over a network
13 13 transport. The current implementation uses the ZeroMQ_ library for messaging
14 14 within and between hosts.
15 15
16 16 .. Note::
17 17
18 18 This document should be considered the authoritative description of the
19 19 IPython messaging protocol, and all developers are strongly encouraged to
20 20 keep it updated as the implementation evolves, so that we have a single
21 21 common reference for all protocol details.
22 22
23 23 The basic design is explained in the following diagram:
24 24
25 25 .. image:: frontend-kernel.png
26 26 :width: 450px
27 27 :alt: IPython kernel/frontend messaging architecture.
28 28 :align: center
29 29 :target: ../_images/frontend-kernel.png
30 30
31 31 A single kernel can be simultaneously connected to one or more frontends. The
32 32 kernel has three sockets that serve the following functions:
33 33
34 34 1. REQ: this socket is connected to a *single* frontend at a time, and it allows
35 35 the kernel to request input from a frontend when :func:`raw_input` is called.
36 36 The frontend holding the matching REP socket acts as a 'virtual keyboard'
37 37 for the kernel while this communication is happening (illustrated in the
38 38 figure by the black outline around the central keyboard). In practice,
39 39 frontends may display such kernel requests using a special input widget or
40 40 otherwise indicating that the user is to type input for the kernel instead
41 41 of normal commands in the frontend.
42 42
43 43 2. XREP: this single sockets allows multiple incoming connections from
44 44 frontends, and this is the socket where requests for code execution, object
45 45 information, prompts, etc. are made to the kernel by any frontend. The
46 46 communication on this socket is a sequence of request/reply actions from
47 47 each frontend and the kernel.
48 48
49 49 3. PUB: this socket is the 'broadcast channel' where the kernel publishes all
50 50 side effects (stdout, stderr, etc.) as well as the requests coming from any
51 51 client over the XREP socket and its own requests on the REP socket. There
52 52 are a number of actions in Python which generate side effects: :func:`print`
53 53 writes to ``sys.stdout``, errors generate tracebacks, etc. Additionally, in
54 54 a multi-client scenario, we want all frontends to be able to know what each
55 55 other has sent to the kernel (this can be useful in collaborative scenarios,
56 56 for example). This socket allows both side effects and the information
57 57 about communications taking place with one client over the XREQ/XREP channel
58 58 to be made available to all clients in a uniform manner.
59 59
60 60 All messages are tagged with enough information (details below) for clients
61 61 to know which messages come from their own interaction with the kernel and
62 62 which ones are from other clients, so they can display each type
63 63 appropriately.
64 64
65 65 The actual format of the messages allowed on each of these channels is
66 66 specified below. Messages are dicts of dicts with string keys and values that
67 67 are reasonably representable in JSON. Our current implementation uses JSON
68 68 explicitly as its message format, but this shouldn't be considered a permanent
69 69 feature. As we've discovered that JSON has non-trivial performance issues due
70 70 to excessive copying, we may in the future move to a pure pickle-based raw
71 71 message format. However, it should be possible to easily convert from the raw
72 72 objects to JSON, since we may have non-python clients (e.g. a web frontend).
73 73 As long as it's easy to make a JSON version of the objects that is a faithful
74 74 representation of all the data, we can communicate with such clients.
75 75
76 76 .. Note::
77 77
78 78 Not all of these have yet been fully fleshed out, but the key ones are, see
79 79 kernel and frontend files for actual implementation details.
80 80
81 81
82 82 Python functional API
83 83 =====================
84 84
85 85 As messages are dicts, they map naturally to a ``func(**kw)`` call form. We
86 86 should develop, at a few key points, functional forms of all the requests that
87 87 take arguments in this manner and automatically construct the necessary dict
88 88 for sending.
89 89
90 90
91 91 General Message Format
92 92 ======================
93 93
94 94 All messages send or received by any IPython process should have the following
95 95 generic structure::
96 96
97 97 {
98 98 # The message header contains a pair of unique identifiers for the
99 99 # originating session and the actual message id, in addition to the
100 100 # username for the process that generated the message. This is useful in
101 101 # collaborative settings where multiple users may be interacting with the
102 102 # same kernel simultaneously, so that frontends can label the various
103 103 # messages in a meaningful way.
104 104 'header' : { 'msg_id' : uuid,
105 105 'username' : str,
106 106 'session' : uuid
107 107 },
108 108
109 109 # In a chain of messages, the header from the parent is copied so that
110 110 # clients can track where messages come from.
111 111 'parent_header' : dict,
112 112
113 113 # All recognized message type strings are listed below.
114 114 'msg_type' : str,
115 115
116 116 # The actual content of the message must be a dict, whose structure
117 117 # depends on the message type.x
118 118 'content' : dict,
119 119 }
120 120
121 121 For each message type, the actual content will differ and all existing message
122 122 types are specified in what follows of this document.
123 123
124 124
125 125 Messages on the XREP/XREQ socket
126 126 ================================
127 127
128 128 .. _execute:
129 129
130 130 Execute
131 131 -------
132 132
133 133 The execution request contains a single string, but this may be a multiline
134 134 string. The kernel is responsible for splitting this into possibly more than
135 135 one block and deciding whether to compile these in 'single' or 'exec' mode.
136 136 We're still sorting out this policy. The current inputsplitter is capable of
137 137 splitting the input for blocks that can all be run as 'single', but in the long
138 138 run it may prove cleaner to only use 'single' mode for truly single-line
139 139 inputs, and run all multiline input in 'exec' mode. This would preserve the
140 140 natural behavior of single-line inputs while allowing long cells to behave more
141 141 likea a script. This design will be refined as we complete the implementation.
142 142
143 143 Message type: ``execute_request``::
144 144
145 145 content = {
146 146 # Source code to be executed by the kernel, one or more lines.
147 147 'code' : str,
148 148
149 149 # A boolean flag which, if True, signals the kernel to execute this
150 150 # code as quietly as possible. This means that the kernel will compile
151 151 # the code with 'exec' instead of 'single' (so sys.displayhook will not
152 152 # fire), and will *not*:
153 153 # - broadcast exceptions on the PUB socket
154 154 # - do any logging
155 155 # - populate any history
156 156 # The default is False.
157 157 'silent' : bool,
158 158 }
159 159
160 160 Upon execution, the kernel *always* sends a reply, with a status code
161 161 indicating what happened and additional data depending on the outcome.
162 162
163 163 Message type: ``execute_reply``::
164 164
165 165 content = {
166 166 # One of: 'ok' OR 'error' OR 'abort'
167 167 'status' : str,
168 168
169 169 # This has the same structure as the output of a prompt request, but is
170 170 # for the client to set up the *next* prompt (with identical limitations
171 171 # to a prompt request)
172 172 'next_prompt' : {
173 173 'prompt_string' : str,
174 174 'prompt_number' : int,
175 175 'input_sep' : str
176 176 },
177 177
178 178 # The prompt number of the actual execution for this code, which may be
179 179 # different from the one used when the code was typed, which was the
180 180 # 'next_prompt' field of the *previous* request. They will differ in the
181 181 # case where there is more than one client talking simultaneously to a
182 182 # kernel, since the numbers can go out of sync. GUI clients can use this
183 183 # to correct the previously written number in-place, terminal ones may
184 184 # re-print a corrected one if desired.
185 185 'prompt_number' : int,
186 186 }
187 187
188 188 When status is 'ok', the following extra fields are present::
189 189
190 190 {
191 191 # The kernel will often transform the input provided to it. This
192 192 # contains the transformed code, which is what was actually executed.
193 193 'transformed_code' : str,
194 194
195 195 # The execution payload is a dict with string keys that may have been
196 196 # produced by the code being executed. It is retrieved by the kernel at
197 197 # the end of the execution and sent back to the front end, which can take
198 198 # action on it as needed. See main text for further details.
199 199 'payload' : dict,
200 200 }
201 201
202 202 .. admonition:: Execution payloads
203 203
204 204 The notion of an 'execution payload' is different from a return value of a
205 205 given set of code, which normally is just displayed on the pyout stream
206 206 through the PUB socket. The idea of a payload is to allow special types of
207 207 code, typically magics, to populate a data container in the IPython kernel
208 208 that will be shipped back to the caller via this channel. The kernel will
209 209 have an API for this, probably something along the lines of::
210 210
211 211 ip.exec_payload_add(key, value)
212 212
213 213 though this API is still in the design stages. The data returned in this
214 214 payload will allow frontends to present special views of what just happened.
215 215
216 216
217 217 When status is 'error', the following extra fields are present::
218 218
219 219 {
220 220 'exc_name' : str, # Exception name, as a string
221 221 'exc_value' : str, # Exception value, as a string
222 222
223 223 # The traceback will contain a list of frames, represented each as a
224 224 # string. For now we'll stick to the existing design of ultraTB, which
225 225 # controls exception level of detail statefully. But eventually we'll
226 226 # want to grow into a model where more information is collected and
227 227 # packed into the traceback object, with clients deciding how little or
228 228 # how much of it to unpack. But for now, let's start with a simple list
229 229 # of strings, since that requires only minimal changes to ultratb as
230 230 # written.
231 231 'traceback' : list,
232 232 }
233 233
234 234
235 235 When status is 'abort', there are for now no additional data fields. This
236 236 happens when the kernel was interrupted by a signal.
237 237
238 238
239 239 Prompt
240 240 ------
241 241
242 242 A simple request for a current prompt string.
243 243
244 244 Message type: ``prompt_request``::
245 245
246 246 content = {}
247 247
248 248 In the reply, the prompt string comes back with the prompt number placeholder
249 249 *unevaluated*. The message format is:
250 250
251 251 Message type: ``prompt_reply``::
252 252
253 253 content = {
254 254 'prompt_string' : str,
255 255 'prompt_number' : int,
256 'input_sep' : str
256 257 }
257 258
258 259 Clients can produce a prompt with ``prompt_string.format(prompt_number)``, but
259 260 they should be aware that the actual prompt number for that input could change
260 261 later, in the case where multiple clients are interacting with a single
261 262 kernel.
262 263
263 264
264 265 Object information
265 266 ------------------
266 267
267 268 One of IPython's most used capabilities is the introspection of Python objects
268 269 in the user's namespace, typically invoked via the ``?`` and ``??`` characters
269 270 (which in reality are shorthands for the ``%pinfo`` magic). This is used often
270 271 enough that it warrants an explicit message type, especially because frontends
271 272 may want to get object information in response to user keystrokes (like Tab or
272 273 F1) besides from the user explicitly typing code like ``x??``.
273 274
274 275 Message type: ``object_info_request``::
275 276
276 277 content = {
277 278 # The (possibly dotted) name of the object to be searched in all
278 279 # relevant namespaces
279 280 'name' : str,
280 281
281 282 # The level of detail desired. The default (0) is equivalent to typing
282 283 # 'x?' at the prompt, 1 is equivalent to 'x??'.
283 284 'detail_level' : int,
284 285 }
285 286
286 287 The returned information will be a dictionary with keys very similar to the
287 288 field names that IPython prints at the terminal.
288 289
289 290 Message type: ``object_info_reply``::
290 291
291 292 content = {
292 293 # Flags for magics and system aliases
293 294 'ismagic' : bool,
294 295 'isalias' : bool,
295 296
296 297 # The name of the namespace where the object was found ('builtin',
297 298 # 'magics', 'alias', 'interactive', etc.)
298 299 'namespace' : str,
299 300
300 301 # The type name will be type.__name__ for normal Python objects, but it
301 302 # can also be a string like 'Magic function' or 'System alias'
302 303 'type_name' : str,
303 304
304 305 'string_form' : str,
305 306
306 307 # For objects with a __class__ attribute this will be set
307 308 'base_class' : str,
308 309
309 310 # For objects with a __len__ attribute this will be set
310 311 'length' : int,
311 312
312 313 # If the object is a function, class or method whose file we can find,
313 314 # we give its full path
314 315 'file' : str,
315 316
316 317 # For pure Python callable objects, we can reconstruct the object
317 318 # definition line which provides its call signature
318 319 'definition' : str,
319 320
320 321 # For instances, provide the constructor signature (the definition of
321 322 # the __init__ method):
322 323 'init_definition' : str,
323 324
324 325 # Docstrings: for any object (function, method, module, package) with a
325 326 # docstring, we show it. But in addition, we may provide additional
326 327 # docstrings. For example, for instances we will show the constructor
327 328 # and class docstrings as well, if available.
328 329 'docstring' : str,
329 330
330 331 # For instances, provide the constructor and class docstrings
331 332 'init_docstring' : str,
332 333 'class_docstring' : str,
333 334
334 335 # If detail_level was 1, we also try to find the source code that
335 336 # defines the object, if possible. The string 'None' will indicate
336 337 # that no source was found.
337 338 'source' : str,
338 339 }
339 340
340 341
341 342 Complete
342 343 --------
343 344
344 345 Message type: ``complete_request``::
345 346
346 347 content = {
347 348 # The text to be completed, such as 'a.is'
348 349 'text' : str,
349 350
350 351 # The full line, such as 'print a.is'. This allows completers to
351 352 # make decisions that may require information about more than just the
352 353 # current word.
353 354 'line' : str,
354 355
355 356 # The entire block of text where the line is. This may be useful in the
356 357 # case of multiline completions where more context may be needed. Note: if
357 358 # in practice this field proves unnecessary, remove it to lighten the
358 359 # messages.
359 360
360 361 'block' : str,
361 362
362 363 # The position of the cursor where the user hit 'TAB' on the line.
363 364 'cursor_pos' : int,
364 365 }
365 366
366 367 Message type: ``complete_reply``::
367 368
368 369 content = {
369 370 # The list of all matches to the completion request, such as
370 371 # ['a.isalnum', 'a.isalpha'] for the above example.
371 372 'matches' : list
372 373 }
373 374
374 375
375 376 History
376 377 -------
377 378
378 379 For clients to explicitly request history from a kernel. The kernel has all
379 380 the actual execution history stored in a single location, so clients can
380 381 request it from the kernel when needed.
381 382
382 383 Message type: ``history_request``::
383 384
384 385 content = {
385 386
386 387 # If True, also return output history in the resulting dict.
387 388 'output' : bool,
388 389
389 390 # If True, return the raw input history, else the transformed input.
390 391 'raw' : bool,
391 392
392 393 # This parameter can be one of: A number, a pair of numbers, None
393 394 # If not given, last 40 are returned.
394 395 # - number n: return the last n entries.
395 396 # - pair n1, n2: return entries in the range(n1, n2).
396 397 # - None: return all history
397 'range' : n or (n1, n2) or None,
398
399 # If a filter is given, it is treated as a regular expression and only
400 # matching entries are returned. re.search() is used to find matches.
401 'filter' : str,
398 'index' : n or (n1, n2) or None,
402 399 }
403 400
404 401 Message type: ``history_reply``::
405 402
406 403 content = {
407 404 # A dict with prompt numbers as keys and either (input, output) or input
408 405 # as the value depending on whether output was True or False,
409 406 # respectively.
410 407 'history' : dict,
411 408 }
412 409 Messages on the PUB/SUB socket
413 410 ==============================
414 411
415 412 Streams (stdout, stderr, etc)
416 413 ------------------------------
417 414
418 415 Message type: ``stream``::
419 416
420 417 content = {
421 418 # The name of the stream is one of 'stdin', 'stdout', 'stderr'
422 419 'name' : str,
423 420
424 421 # The data is an arbitrary string to be written to that stream
425 422 'data' : str,
426 423 }
427 424
428 425 When a kernel receives a raw_input call, it should also broadcast it on the pub
429 426 socket with the names 'stdin' and 'stdin_reply'. This will allow other clients
430 427 to monitor/display kernel interactions and possibly replay them to their user
431 428 or otherwise expose them.
432 429
433 430 Python inputs
434 431 -------------
435 432
436 433 These messages are the re-broadcast of the ``execute_request``.
437 434
438 435 Message type: ``pyin``::
439 436
440 437 content = {
441 438 # Source code to be executed, one or more lines
442 439 'code' : str
443 440 }
444 441
445 442 Python outputs
446 443 --------------
447 444
448 445 When Python produces output from code that has been compiled in with the
449 446 'single' flag to :func:`compile`, any expression that produces a value (such as
450 447 ``1+1``) is passed to ``sys.displayhook``, which is a callable that can do with
451 448 this value whatever it wants. The default behavior of ``sys.displayhook`` in
452 449 the Python interactive prompt is to print to ``sys.stdout`` the :func:`repr` of
453 450 the value as long as it is not ``None`` (which isn't printed at all). In our
454 451 case, the kernel instantiates as ``sys.displayhook`` an object which has
455 452 similar behavior, but which instead of printing to stdout, broadcasts these
456 453 values as ``pyout`` messages for clients to display appropriately.
457 454
458 455 Message type: ``pyout``::
459 456
460 457 content = {
461 458 # The data is typically the repr() of the object.
462 459 'data' : str,
463 460
464 461 # The prompt number for this execution is also provided so that clients
465 462 # can display it, since IPython automatically creates variables called
466 463 # _N (for prompt N).
467 464 'prompt_number' : int,
468 465 }
469 466
470 467 Python errors
471 468 -------------
472 469
473 470 When an error occurs during code execution
474 471
475 472 Message type: ``pyerr``::
476 473
477 474 content = {
478 475 # Similar content to the execute_reply messages for the 'error' case,
479 476 # except the 'status' field is omitted.
480 477 }
481 478
482 479 Kernel crashes
483 480 --------------
484 481
485 482 When the kernel has an unexpected exception, caught by the last-resort
486 483 sys.excepthook, we should broadcast the crash handler's output before exiting.
487 484 This will allow clients to notice that a kernel died, inform the user and
488 485 propose further actions.
489 486
490 487 Message type: ``crash``::
491 488
492 489 content = {
493 490 # Similarly to the 'error' case for execute_reply messages, this will
494 491 # contain exc_name, exc_type and traceback fields.
495 492
496 493 # An additional field with supplementary information such as where to
497 494 # send the crash message
498 495 'info' : str,
499 496 }
500 497
501 498
502 499 Future ideas
503 500 ------------
504 501
505 502 Other potential message types, currently unimplemented, listed below as ideas.
506 503
507 504 Message type: ``file``::
508 505
509 506 content = {
510 507 'path' : 'cool.jpg',
511 508 'mimetype' : str,
512 509 'data' : str,
513 510 }
514 511
515 512
516 513 Messages on the REQ/REP socket
517 514 ==============================
518 515
519 516 This is a socket that goes in the opposite direction: from the kernel to a
520 517 *single* frontend, and its purpose is to allow ``raw_input`` and similar
521 518 operations that read from ``sys.stdin`` on the kernel to be fulfilled by the
522 519 client. For now we will keep these messages as simple as possible, since they
523 520 basically only mean to convey the ``raw_input(prompt)`` call.
524 521
525 522 Message type: ``input_request``::
526 523
527 524 content = { 'prompt' : str }
528 525
529 526 Message type: ``input_reply``::
530 527
531 528 content = { 'value' : str }
532 529
533 530 .. Note::
534 531
535 532 We do not explicitly try to forward the raw ``sys.stdin`` object, because in
536 533 practice the kernel should behave like an interactive program. When a
537 534 program is opened on the console, the keyboard effectively takes over the
538 535 ``stdin`` file descriptor, and it can't be used for raw reading anymore.
539 536 Since the IPython kernel effectively behaves like a console program (albeit
540 537 one whose "keyboard" is actually living in a separate process and
541 538 transported over the zmq connection), raw ``stdin`` isn't expected to be
542 539 available.
543 540
544 541
545 542 Heartbeat for kernels
546 543 =====================
547 544
548 545 Initially we had considered using messages like those above over ZMQ for a
549 546 kernel 'heartbeat' (a way to detect quickly and reliably whether a kernel is
550 547 alive at all, even if it may be busy executing user code). But this has the
551 548 problem that if the kernel is locked inside extension code, it wouldn't execute
552 549 the python heartbeat code. But it turns out that we can implement a basic
553 550 heartbeat with pure ZMQ, without using any Python messaging at all.
554 551
555 552 The monitor sends out a single zmq message (right now, it is a str of the
556 553 monitor's lifetime in seconds), and gets the same message right back, prefixed
557 554 with the zmq identity of the XREQ socket in the heartbeat process. This can be
558 555 a uuid, or even a full message, but there doesn't seem to be a need for packing
559 556 up a message when the sender and receiver are the exact same Python object.
560 557
561 558 The model is this::
562 559
563 560 monitor.send(str(self.lifetime)) # '1.2345678910'
564 561
565 562 and the monitor receives some number of messages of the form::
566 563
567 564 ['uuid-abcd-dead-beef', '1.2345678910']
568 565
569 566 where the first part is the zmq.IDENTITY of the heart's XREQ on the engine, and
570 567 the rest is the message sent by the monitor. No Python code ever has any
571 568 access to the message between the monitor's send, and the monitor's recv.
572 569
573 570
574 571 ToDo
575 572 ====
576 573
577 574 Missing things include:
578 575
579 576 * Important: finish thinking through the payload concept and API.
580 577
581 578 * Important: ensure that we have a good solution for magics like %edit. It's
582 579 likely that with the payload concept we can build a full solution, but not
583 580 100% clear yet.
584 581
585 582 * Finishing the details of the heartbeat protocol.
586 583
587 584 * Signal handling: specify what kind of information kernel should broadcast (or
588 585 not) when it receives signals.
589 586
590 587 .. include:: ../links.rst
General Comments 0
You need to be logged in to leave comments. Login now