##// END OF EJS Templates
Fixed bugs with raw_input and finished and implementing InStream.
epatters -
Show More
@@ -1,883 +1,890 b''
1 1 # Standard library imports
2 2 import re
3 3 import sys
4 4
5 5 # System library imports
6 6 from PyQt4 import QtCore, QtGui
7 7
8 8 # Local imports
9 9 from completion_widget import CompletionWidget
10 10
11 11
12 12 class AnsiCodeProcessor(object):
13 13 """ Translates ANSI escape codes into readable attributes.
14 14 """
15 15
16 16 def __init__(self):
17 17 self.ansi_colors = ( # Normal, Bright/Light
18 18 ('#000000', '#7f7f7f'), # 0: black
19 19 ('#cd0000', '#ff0000'), # 1: red
20 20 ('#00cd00', '#00ff00'), # 2: green
21 21 ('#cdcd00', '#ffff00'), # 3: yellow
22 22 ('#0000ee', '#0000ff'), # 4: blue
23 23 ('#cd00cd', '#ff00ff'), # 5: magenta
24 24 ('#00cdcd', '#00ffff'), # 6: cyan
25 25 ('#e5e5e5', '#ffffff')) # 7: white
26 26 self.reset()
27 27
28 28 def set_code(self, code):
29 29 """ Set attributes based on code.
30 30 """
31 31 if code == 0:
32 32 self.reset()
33 33 elif code == 1:
34 34 self.intensity = 1
35 35 self.bold = True
36 36 elif code == 3:
37 37 self.italic = True
38 38 elif code == 4:
39 39 self.underline = True
40 40 elif code == 22:
41 41 self.intensity = 0
42 42 self.bold = False
43 43 elif code == 23:
44 44 self.italic = False
45 45 elif code == 24:
46 46 self.underline = False
47 47 elif code >= 30 and code <= 37:
48 48 self.foreground_color = code - 30
49 49 elif code == 39:
50 50 self.foreground_color = None
51 51 elif code >= 40 and code <= 47:
52 52 self.background_color = code - 40
53 53 elif code == 49:
54 54 self.background_color = None
55 55
56 56 def reset(self):
57 57 """ Reset attributs to their default values.
58 58 """
59 59 self.intensity = 0
60 60 self.italic = False
61 61 self.bold = False
62 62 self.underline = False
63 63 self.foreground_color = None
64 64 self.background_color = None
65 65
66 66
67 67 class QtAnsiCodeProcessor(AnsiCodeProcessor):
68 68 """ Translates ANSI escape codes into QTextCharFormats.
69 69 """
70 70
71 71 def get_format(self):
72 72 """ Returns a QTextCharFormat that encodes the current style attributes.
73 73 """
74 74 format = QtGui.QTextCharFormat()
75 75
76 76 # Set foreground color
77 77 if self.foreground_color is not None:
78 78 color = self.ansi_colors[self.foreground_color][self.intensity]
79 79 format.setForeground(QtGui.QColor(color))
80 80
81 81 # Set background color
82 82 if self.background_color is not None:
83 83 color = self.ansi_colors[self.background_color][self.intensity]
84 84 format.setBackground(QtGui.QColor(color))
85 85
86 86 # Set font weight/style options
87 87 if self.bold:
88 88 format.setFontWeight(QtGui.QFont.Bold)
89 89 else:
90 90 format.setFontWeight(QtGui.QFont.Normal)
91 91 format.setFontItalic(self.italic)
92 92 format.setFontUnderline(self.underline)
93 93
94 94 return format
95 95
96 96
97 97 class ConsoleWidget(QtGui.QPlainTextEdit):
98 98 """ Base class for console-type widgets. This class is mainly concerned with
99 99 dealing with the prompt, keeping the cursor inside the editing line, and
100 100 handling ANSI escape sequences.
101 101 """
102 102
103 103 # Whether to process ANSI escape codes.
104 104 ansi_codes = True
105 105
106 106 # The maximum number of lines of text before truncation.
107 107 buffer_size = 500
108 108
109 109 # Whether to use a CompletionWidget or plain text output for tab completion.
110 110 gui_completion = True
111 111
112 112 # Whether to override ShortcutEvents for the keybindings defined by this
113 113 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
114 114 # priority (when it has focus) over, e.g., window-level menu shortcuts.
115 115 override_shortcuts = False
116 116
117 117 # Protected class variables.
118 118 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
119 119 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
120 120 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
121 121 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
122 122 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
123 123 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
124 124 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
125 125 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
126 126 _shortcuts = set(_ctrl_down_remap.keys() +
127 127 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
128 128
129 129 #---------------------------------------------------------------------------
130 130 # 'QObject' interface
131 131 #---------------------------------------------------------------------------
132 132
133 133 def __init__(self, parent=None):
134 134 QtGui.QPlainTextEdit.__init__(self, parent)
135 135
136 136 # Initialize protected variables.
137 137 self._ansi_processor = QtAnsiCodeProcessor()
138 138 self._completion_widget = CompletionWidget(self)
139 139 self._continuation_prompt = '> '
140 140 self._executing = False
141 141 self._prompt = ''
142 142 self._prompt_pos = 0
143 143 self._reading = False
144 144 self._reading_callback = None
145 145
146 146 # Set a monospaced font.
147 147 self.reset_font()
148 148
149 149 # Define a custom context menu.
150 150 self._context_menu = QtGui.QMenu(self)
151 151
152 152 copy_action = QtGui.QAction('Copy', self)
153 153 copy_action.triggered.connect(self.copy)
154 154 self.copyAvailable.connect(copy_action.setEnabled)
155 155 self._context_menu.addAction(copy_action)
156 156
157 157 self._paste_action = QtGui.QAction('Paste', self)
158 158 self._paste_action.triggered.connect(self.paste)
159 159 self._context_menu.addAction(self._paste_action)
160 160 self._context_menu.addSeparator()
161 161
162 162 select_all_action = QtGui.QAction('Select All', self)
163 163 select_all_action.triggered.connect(self.selectAll)
164 164 self._context_menu.addAction(select_all_action)
165 165
166 166 def event(self, event):
167 167 """ Reimplemented to override shortcuts, if necessary.
168 168 """
169 169 # On Mac OS, it is always unnecessary to override shortcuts, hence the
170 170 # check below. Users should just use the Control key instead of the
171 171 # Command key.
172 172 if self.override_shortcuts and \
173 173 sys.platform != 'darwin' and \
174 174 event.type() == QtCore.QEvent.ShortcutOverride and \
175 175 self._control_down(event.modifiers()) and \
176 176 event.key() in self._shortcuts:
177 177 event.accept()
178 178 return True
179 179 else:
180 180 return QtGui.QPlainTextEdit.event(self, event)
181 181
182 182 #---------------------------------------------------------------------------
183 183 # 'QWidget' interface
184 184 #---------------------------------------------------------------------------
185 185
186 186 def contextMenuEvent(self, event):
187 187 """ Reimplemented to create a menu without destructive actions like
188 188 'Cut' and 'Delete'.
189 189 """
190 190 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
191 191 self._paste_action.setEnabled(not clipboard_empty)
192 192
193 193 self._context_menu.exec_(event.globalPos())
194 194
195 195 def dragMoveEvent(self, event):
196 196 """ Reimplemented to disable dropping text.
197 197 """
198 198 event.ignore()
199 199
200 200 def keyPressEvent(self, event):
201 201 """ Reimplemented to create a console-like interface.
202 202 """
203 203 intercepted = False
204 204 cursor = self.textCursor()
205 205 position = cursor.position()
206 206 key = event.key()
207 207 ctrl_down = self._control_down(event.modifiers())
208 208 alt_down = event.modifiers() & QtCore.Qt.AltModifier
209 209 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
210 210
211 211 # Even though we have reimplemented 'paste', the C++ level slot is still
212 212 # called by Qt. So we intercept the key press here.
213 213 if event.matches(QtGui.QKeySequence.Paste):
214 214 self.paste()
215 215 intercepted = True
216 216
217 217 elif ctrl_down:
218 218 if key in self._ctrl_down_remap:
219 219 ctrl_down = False
220 220 key = self._ctrl_down_remap[key]
221 221 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
222 222 QtCore.Qt.NoModifier)
223 223
224 224 elif key == QtCore.Qt.Key_K:
225 225 if self._in_buffer(position):
226 226 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
227 227 QtGui.QTextCursor.KeepAnchor)
228 228 cursor.removeSelectedText()
229 229 intercepted = True
230 230
231 231 elif key == QtCore.Qt.Key_X:
232 232 intercepted = True
233 233
234 234 elif key == QtCore.Qt.Key_Y:
235 235 self.paste()
236 236 intercepted = True
237 237
238 238 elif alt_down:
239 239 if key == QtCore.Qt.Key_B:
240 240 self.setTextCursor(self._get_word_start_cursor(position))
241 241 intercepted = True
242 242
243 243 elif key == QtCore.Qt.Key_F:
244 244 self.setTextCursor(self._get_word_end_cursor(position))
245 245 intercepted = True
246 246
247 247 elif key == QtCore.Qt.Key_Backspace:
248 248 cursor = self._get_word_start_cursor(position)
249 249 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
250 250 cursor.removeSelectedText()
251 251 intercepted = True
252 252
253 253 elif key == QtCore.Qt.Key_D:
254 254 cursor = self._get_word_end_cursor(position)
255 255 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
256 256 cursor.removeSelectedText()
257 257 intercepted = True
258 258
259 259 if self._completion_widget.isVisible():
260 260 self._completion_widget.keyPressEvent(event)
261 261 intercepted = event.isAccepted()
262 262
263 263 else:
264 264 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
265 265 if self._reading:
266 266 self.appendPlainText('\n')
267 267 self._reading = False
268 268 if self._reading_callback:
269 269 self._reading_callback()
270 270 self._reading_callback = None
271 271 elif not self._executing:
272 272 self.execute(interactive=True)
273 273 intercepted = True
274 274
275 275 elif key == QtCore.Qt.Key_Up:
276 276 if self._reading or not self._up_pressed():
277 277 intercepted = True
278 278 else:
279 279 prompt_line = self._get_prompt_cursor().blockNumber()
280 280 intercepted = cursor.blockNumber() <= prompt_line
281 281
282 282 elif key == QtCore.Qt.Key_Down:
283 283 if self._reading or not self._down_pressed():
284 284 intercepted = True
285 285 else:
286 286 end_line = self._get_end_cursor().blockNumber()
287 287 intercepted = cursor.blockNumber() == end_line
288 288
289 289 elif key == QtCore.Qt.Key_Tab:
290 290 if self._reading:
291 291 intercepted = False
292 292 else:
293 293 intercepted = not self._tab_pressed()
294 294
295 295 elif key == QtCore.Qt.Key_Left:
296 296 intercepted = not self._in_buffer(position - 1)
297 297
298 298 elif key == QtCore.Qt.Key_Home:
299 299 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
300 300 start_pos = cursor.position()
301 301 start_line = cursor.blockNumber()
302 302 if start_line == self._get_prompt_cursor().blockNumber():
303 303 start_pos += len(self._prompt)
304 304 else:
305 305 start_pos += len(self._continuation_prompt)
306 306 if shift_down and self._in_buffer(position):
307 307 self._set_selection(position, start_pos)
308 308 else:
309 309 self._set_position(start_pos)
310 310 intercepted = True
311 311
312 312 elif key == QtCore.Qt.Key_Backspace and not alt_down:
313 313
314 314 # Line deletion (remove continuation prompt)
315 315 len_prompt = len(self._continuation_prompt)
316 316 if not self._reading and \
317 317 cursor.columnNumber() == len_prompt and \
318 318 position != self._prompt_pos:
319 319 cursor.setPosition(position - len_prompt,
320 320 QtGui.QTextCursor.KeepAnchor)
321 321 cursor.removeSelectedText()
322 322
323 323 # Regular backwards deletion
324 324 else:
325 325 anchor = cursor.anchor()
326 326 if anchor == position:
327 327 intercepted = not self._in_buffer(position - 1)
328 328 else:
329 329 intercepted = not self._in_buffer(min(anchor, position))
330 330
331 331 elif key == QtCore.Qt.Key_Delete:
332 332 anchor = cursor.anchor()
333 333 intercepted = not self._in_buffer(min(anchor, position))
334 334
335 335 # Don't move cursor if control is down to allow copy-paste using
336 336 # the keyboard in any part of the buffer.
337 337 if not ctrl_down:
338 338 self._keep_cursor_in_buffer()
339 339
340 340 if not intercepted:
341 341 QtGui.QPlainTextEdit.keyPressEvent(self, event)
342 342
343 343 #--------------------------------------------------------------------------
344 344 # 'QPlainTextEdit' interface
345 345 #--------------------------------------------------------------------------
346 346
347 347 def appendPlainText(self, text):
348 348 """ Reimplemented to not append text as a new paragraph, which doesn't
349 349 make sense for a console widget. Also, if enabled, handle ANSI
350 350 codes.
351 351 """
352 352 cursor = self.textCursor()
353 353 cursor.movePosition(QtGui.QTextCursor.End)
354 354
355 355 if self.ansi_codes:
356 356 format = QtGui.QTextCharFormat()
357 357 previous_end = 0
358 358 for match in self._ansi_pattern.finditer(text):
359 359 cursor.insertText(text[previous_end:match.start()], format)
360 360 previous_end = match.end()
361 361 for code in match.group(1).split(';'):
362 362 self._ansi_processor.set_code(int(code))
363 363 format = self._ansi_processor.get_format()
364 364 cursor.insertText(text[previous_end:], format)
365 365 else:
366 366 cursor.insertText(text)
367 367
368 368 def clear(self, keep_input=False):
369 369 """ Reimplemented to write a new prompt. If 'keep_input' is set,
370 370 restores the old input buffer when the new prompt is written.
371 371 """
372 372 super(ConsoleWidget, self).clear()
373 373
374 374 if keep_input:
375 375 input_buffer = self.input_buffer
376 376 self._show_prompt()
377 377 if keep_input:
378 378 self.input_buffer = input_buffer
379 379
380 380 def paste(self):
381 381 """ Reimplemented to ensure that text is pasted in the editing region.
382 382 """
383 383 self._keep_cursor_in_buffer()
384 384 QtGui.QPlainTextEdit.paste(self)
385 385
386 386 def print_(self, printer):
387 387 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
388 388 slot has the wrong signature.
389 389 """
390 390 QtGui.QPlainTextEdit.print_(self, printer)
391 391
392 392 #---------------------------------------------------------------------------
393 393 # 'ConsoleWidget' public interface
394 394 #---------------------------------------------------------------------------
395 395
396 396 def execute(self, source=None, hidden=False, interactive=False):
397 397 """ Executes source or the input buffer, possibly prompting for more
398 398 input.
399 399
400 400 Parameters:
401 401 -----------
402 402 source : str, optional
403 403
404 404 The source to execute. If not specified, the input buffer will be
405 405 used. If specified and 'hidden' is False, the input buffer will be
406 406 replaced with the source before execution.
407 407
408 408 hidden : bool, optional (default False)
409 409
410 410 If set, no output will be shown and the prompt will not be modified.
411 411 In other words, it will be completely invisible to the user that
412 412 an execution has occurred.
413 413
414 414 interactive : bool, optional (default False)
415 415
416 416 Whether the console is to treat the source as having been manually
417 417 entered by the user. The effect of this parameter depends on the
418 418 subclass implementation.
419 419
420 420 Raises:
421 421 -------
422 422 RuntimeError
423 423 If incomplete input is given and 'hidden' is True. In this case,
424 424 it not possible to prompt for more input.
425 425
426 426 Returns:
427 427 --------
428 428 A boolean indicating whether the source was executed.
429 429 """
430 430 if not hidden:
431 431 if source is not None:
432 432 self.input_buffer = source
433 433
434 434 self.appendPlainText('\n')
435 435 self._executing_input_buffer = self.input_buffer
436 436 self._executing = True
437 437 self._prompt_finished()
438 438
439 439 real_source = self.input_buffer if source is None else source
440 440 complete = self._is_complete(real_source, interactive)
441 441 if complete:
442 442 if not hidden:
443 443 # The maximum block count is only in effect during execution.
444 444 # This ensures that _prompt_pos does not become invalid due to
445 445 # text truncation.
446 446 self.setMaximumBlockCount(self.buffer_size)
447 447 self._execute(real_source, hidden)
448 448 elif hidden:
449 449 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
450 450 else:
451 451 self._show_continuation_prompt()
452 452
453 453 return complete
454 454
455 455 def _get_input_buffer(self):
456 456 """ The text that the user has entered entered at the current prompt.
457 457 """
458 458 # If we're executing, the input buffer may not even exist anymore due to
459 459 # the limit imposed by 'buffer_size'. Therefore, we store it.
460 460 if self._executing:
461 461 return self._executing_input_buffer
462 462
463 463 cursor = self._get_end_cursor()
464 464 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
465 465
466 466 # Use QTextDocumentFragment intermediate object because it strips
467 467 # out the Unicode line break characters that Qt insists on inserting.
468 468 input_buffer = str(cursor.selection().toPlainText())
469 469
470 470 # Strip out continuation prompts.
471 471 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
472 472
473 473 def _set_input_buffer(self, string):
474 474 """ Replaces the text in the input buffer with 'string'.
475 475 """
476 476 # Add continuation prompts where necessary.
477 477 lines = string.splitlines()
478 478 for i in xrange(1, len(lines)):
479 479 lines[i] = self._continuation_prompt + lines[i]
480 480 string = '\n'.join(lines)
481 481
482 482 # Replace buffer with new text.
483 483 cursor = self._get_end_cursor()
484 484 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
485 485 cursor.insertText(string)
486 486 self.moveCursor(QtGui.QTextCursor.End)
487 487
488 488 input_buffer = property(_get_input_buffer, _set_input_buffer)
489 489
490 490 def _get_input_buffer_cursor_line(self):
491 491 """ The text in the line of the input buffer in which the user's cursor
492 492 rests. Returns a string if there is such a line; otherwise, None.
493 493 """
494 494 if self._executing:
495 495 return None
496 496 cursor = self.textCursor()
497 497 if cursor.position() >= self._prompt_pos:
498 498 text = str(cursor.block().text())
499 499 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
500 500 return text[len(self._prompt):]
501 501 else:
502 502 return text[len(self._continuation_prompt):]
503 503 else:
504 504 return None
505 505
506 506 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
507 507
508 508 def _get_font(self):
509 509 """ The base font being used by the ConsoleWidget.
510 510 """
511 511 return self.document().defaultFont()
512 512
513 513 def _set_font(self, font):
514 514 """ Sets the base font for the ConsoleWidget to the specified QFont.
515 515 """
516 516 self._completion_widget.setFont(font)
517 517 self.document().setDefaultFont(font)
518 518
519 519 font = property(_get_font, _set_font)
520 520
521 521 def reset_font(self):
522 522 """ Sets the font to the default fixed-width font for this platform.
523 523 """
524 524 if sys.platform == 'win32':
525 525 name = 'Courier'
526 526 elif sys.platform == 'darwin':
527 527 name = 'Monaco'
528 528 else:
529 529 name = 'Monospace'
530 530 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
531 531 font.setStyleHint(QtGui.QFont.TypeWriter)
532 532 self._set_font(font)
533 533
534 534 #---------------------------------------------------------------------------
535 535 # 'ConsoleWidget' abstract interface
536 536 #---------------------------------------------------------------------------
537 537
538 538 def _is_complete(self, source, interactive):
539 539 """ Returns whether 'source' can be executed. When triggered by an
540 540 Enter/Return key press, 'interactive' is True; otherwise, it is
541 541 False.
542 542 """
543 543 raise NotImplementedError
544 544
545 545 def _execute(self, source, hidden):
546 546 """ Execute 'source'. If 'hidden', do not show any output.
547 547 """
548 548 raise NotImplementedError
549 549
550 550 def _prompt_started_hook(self):
551 551 """ Called immediately after a new prompt is displayed.
552 552 """
553 553 pass
554 554
555 555 def _prompt_finished_hook(self):
556 556 """ Called immediately after a prompt is finished, i.e. when some input
557 557 will be processed and a new prompt displayed.
558 558 """
559 559 pass
560 560
561 561 def _up_pressed(self):
562 562 """ Called when the up key is pressed. Returns whether to continue
563 563 processing the event.
564 564 """
565 565 return True
566 566
567 567 def _down_pressed(self):
568 568 """ Called when the down key is pressed. Returns whether to continue
569 569 processing the event.
570 570 """
571 571 return True
572 572
573 573 def _tab_pressed(self):
574 574 """ Called when the tab key is pressed. Returns whether to continue
575 575 processing the event.
576 576 """
577 577 return False
578 578
579 579 #--------------------------------------------------------------------------
580 580 # 'ConsoleWidget' protected interface
581 581 #--------------------------------------------------------------------------
582 582
583 583 def _control_down(self, modifiers):
584 584 """ Given a KeyboardModifiers flags object, return whether the Control
585 585 key is down (on Mac OS, treat the Command key as a synonym for
586 586 Control).
587 587 """
588 588 down = bool(modifiers & QtCore.Qt.ControlModifier)
589 589
590 590 # Note: on Mac OS, ControlModifier corresponds to the Command key while
591 591 # MetaModifier corresponds to the Control key.
592 592 if sys.platform == 'darwin':
593 593 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
594 594
595 595 return down
596 596
597 597 def _complete_with_items(self, cursor, items):
598 598 """ Performs completion with 'items' at the specified cursor location.
599 599 """
600 600 if len(items) == 1:
601 601 cursor.setPosition(self.textCursor().position(),
602 602 QtGui.QTextCursor.KeepAnchor)
603 603 cursor.insertText(items[0])
604 604 elif len(items) > 1:
605 605 if self.gui_completion:
606 606 self._completion_widget.show_items(cursor, items)
607 607 else:
608 608 text = '\n'.join(items) + '\n'
609 609 self._write_text_keeping_prompt(text)
610 610
611 611 def _get_end_cursor(self):
612 612 """ Convenience method that returns a cursor for the last character.
613 613 """
614 614 cursor = self.textCursor()
615 615 cursor.movePosition(QtGui.QTextCursor.End)
616 616 return cursor
617 617
618 618 def _get_prompt_cursor(self):
619 619 """ Convenience method that returns a cursor for the prompt position.
620 620 """
621 621 cursor = self.textCursor()
622 622 cursor.setPosition(self._prompt_pos)
623 623 return cursor
624 624
625 625 def _get_selection_cursor(self, start, end):
626 626 """ Convenience method that returns a cursor with text selected between
627 627 the positions 'start' and 'end'.
628 628 """
629 629 cursor = self.textCursor()
630 630 cursor.setPosition(start)
631 631 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
632 632 return cursor
633 633
634 634 def _get_word_start_cursor(self, position):
635 635 """ Find the start of the word to the left the given position. If a
636 636 sequence of non-word characters precedes the first word, skip over
637 637 them. (This emulates the behavior of bash, emacs, etc.)
638 638 """
639 639 document = self.document()
640 640 position -= 1
641 641 while self._in_buffer(position) and \
642 642 not document.characterAt(position).isLetterOrNumber():
643 643 position -= 1
644 644 while self._in_buffer(position) and \
645 645 document.characterAt(position).isLetterOrNumber():
646 646 position -= 1
647 647 cursor = self.textCursor()
648 648 cursor.setPosition(position + 1)
649 649 return cursor
650 650
651 651 def _get_word_end_cursor(self, position):
652 652 """ Find the end of the word to the right the given position. If a
653 653 sequence of non-word characters precedes the first word, skip over
654 654 them. (This emulates the behavior of bash, emacs, etc.)
655 655 """
656 656 document = self.document()
657 657 end = self._get_end_cursor().position()
658 658 while position < end and \
659 659 not document.characterAt(position).isLetterOrNumber():
660 660 position += 1
661 661 while position < end and \
662 662 document.characterAt(position).isLetterOrNumber():
663 663 position += 1
664 664 cursor = self.textCursor()
665 665 cursor.setPosition(position)
666 666 return cursor
667 667
668 668 def _prompt_started(self):
669 669 """ Called immediately after a new prompt is displayed.
670 670 """
671 671 # Temporarily disable the maximum block count to permit undo/redo and
672 672 # to ensure that the prompt position does not change due to truncation.
673 673 self.setMaximumBlockCount(0)
674 674 self.setUndoRedoEnabled(True)
675 675
676 676 self.setReadOnly(False)
677 677 self.moveCursor(QtGui.QTextCursor.End)
678 678 self.centerCursor()
679 679
680 680 self._executing = False
681 681 self._prompt_started_hook()
682 682
683 683 def _prompt_finished(self):
684 684 """ Called immediately after a prompt is finished, i.e. when some input
685 685 will be processed and a new prompt displayed.
686 686 """
687 687 self.setUndoRedoEnabled(False)
688 688 self.setReadOnly(True)
689 689 self._prompt_finished_hook()
690 690
691 691 def _readline(self, prompt='', callback=None):
692 692 """ Reads one line of input from the user.
693 693
694 694 Parameters
695 695 ----------
696 696 prompt : str, optional
697 697 The prompt to print before reading the line.
698 698
699 699 callback : callable, optional
700 700 A callback to execute with the read line. If not specified, input is
701 701 read *synchronously* and this method does not return until it has
702 702 been read.
703 703
704 704 Returns
705 705 -------
706 706 If a callback is specified, returns nothing. Otherwise, returns the
707 707 input string with the trailing newline stripped.
708 708 """
709 709 if self._reading:
710 710 raise RuntimeError('Cannot read a line. Widget is already reading.')
711 711
712 712 if not callback and not self.isVisible():
713 713 # If the user cannot see the widget, this function cannot return.
714 714 raise RuntimeError('Cannot synchronously read a line if the widget'
715 715 'is not visible!')
716 716
717 717 self._reading = True
718 self._show_prompt(prompt)
718 self._show_prompt(prompt, newline=False)
719 719
720 720 if callback is None:
721 721 self._reading_callback = None
722 722 while self._reading:
723 723 QtCore.QCoreApplication.processEvents()
724 724 return self.input_buffer.rstrip('\n')
725 725
726 726 else:
727 727 self._reading_callback = lambda: \
728 728 callback(self.input_buffer.rstrip('\n'))
729 729
730 730 def _set_position(self, position):
731 731 """ Convenience method to set the position of the cursor.
732 732 """
733 733 cursor = self.textCursor()
734 734 cursor.setPosition(position)
735 735 self.setTextCursor(cursor)
736 736
737 737 def _set_selection(self, start, end):
738 738 """ Convenience method to set the current selected text.
739 739 """
740 740 self.setTextCursor(self._get_selection_cursor(start, end))
741 741
742 def _show_prompt(self, prompt=None):
743 """ Writes a new prompt at the end of the buffer. If 'prompt' is not
744 specified, the previous prompt is used.
745 """
746 # Use QTextDocumentFragment intermediate object because it strips
747 # out the Unicode line break characters that Qt insists on inserting.
748 cursor = self._get_end_cursor()
749 if cursor.position() > 0:
750 cursor.movePosition(QtGui.QTextCursor.Left,
751 QtGui.QTextCursor.KeepAnchor)
752 if str(cursor.selection().toPlainText()) != '\n':
753 self.appendPlainText('\n')
742 def _show_prompt(self, prompt=None, newline=True):
743 """ Writes a new prompt at the end of the buffer.
744
745 Parameters
746 ----------
747 prompt : str, optional
748 The prompt to show. If not specified, the previous prompt is used.
749
750 newline : bool, optional (default True)
751 If set, a new line will be written before showing the prompt if
752 there is not already a newline at the end of the buffer.
753 """
754 if newline:
755 cursor = self._get_end_cursor()
756 if cursor.position() > 0:
757 cursor.movePosition(QtGui.QTextCursor.Left,
758 QtGui.QTextCursor.KeepAnchor)
759 if str(cursor.selection().toPlainText()) != '\n':
760 self.appendPlainText('\n')
754 761
755 762 if prompt is not None:
756 763 self._prompt = prompt
757 764 self.appendPlainText(self._prompt)
758 765
759 766 self._prompt_pos = self._get_end_cursor().position()
760 767 self._prompt_started()
761 768
762 769 def _show_continuation_prompt(self):
763 770 """ Writes a new continuation prompt at the end of the buffer.
764 771 """
765 772 self.appendPlainText(self._continuation_prompt)
766 773 self._prompt_started()
767 774
768 775 def _write_text_keeping_prompt(self, text):
769 776 """ Writes 'text' after the current prompt, then restores the old prompt
770 777 with its old input buffer.
771 778 """
772 779 input_buffer = self.input_buffer
773 780 self.appendPlainText('\n')
774 781 self._prompt_finished()
775 782
776 783 self.appendPlainText(text)
777 784 self._show_prompt()
778 785 self.input_buffer = input_buffer
779 786
780 787 def _in_buffer(self, position):
781 788 """ Returns whether the given position is inside the editing region.
782 789 """
783 790 return position >= self._prompt_pos
784 791
785 792 def _keep_cursor_in_buffer(self):
786 793 """ Ensures that the cursor is inside the editing region. Returns
787 794 whether the cursor was moved.
788 795 """
789 796 cursor = self.textCursor()
790 797 if cursor.position() < self._prompt_pos:
791 798 cursor.movePosition(QtGui.QTextCursor.End)
792 799 self.setTextCursor(cursor)
793 800 return True
794 801 else:
795 802 return False
796 803
797 804
798 805 class HistoryConsoleWidget(ConsoleWidget):
799 806 """ A ConsoleWidget that keeps a history of the commands that have been
800 807 executed.
801 808 """
802 809
803 810 #---------------------------------------------------------------------------
804 811 # 'QObject' interface
805 812 #---------------------------------------------------------------------------
806 813
807 814 def __init__(self, parent=None):
808 815 super(HistoryConsoleWidget, self).__init__(parent)
809 816
810 817 self._history = []
811 818 self._history_index = 0
812 819
813 820 #---------------------------------------------------------------------------
814 821 # 'ConsoleWidget' public interface
815 822 #---------------------------------------------------------------------------
816 823
817 824 def execute(self, source=None, hidden=False, interactive=False):
818 825 """ Reimplemented to the store history.
819 826 """
820 827 if not hidden:
821 828 history = self.input_buffer if source is None else source
822 829
823 830 executed = super(HistoryConsoleWidget, self).execute(
824 831 source, hidden, interactive)
825 832
826 833 if executed and not hidden:
827 834 self._history.append(history.rstrip())
828 835 self._history_index = len(self._history)
829 836
830 837 return executed
831 838
832 839 #---------------------------------------------------------------------------
833 840 # 'ConsoleWidget' abstract interface
834 841 #---------------------------------------------------------------------------
835 842
836 843 def _up_pressed(self):
837 844 """ Called when the up key is pressed. Returns whether to continue
838 845 processing the event.
839 846 """
840 847 prompt_cursor = self._get_prompt_cursor()
841 848 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
842 849 self.history_previous()
843 850
844 851 # Go to the first line of prompt for seemless history scrolling.
845 852 cursor = self._get_prompt_cursor()
846 853 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
847 854 self.setTextCursor(cursor)
848 855
849 856 return False
850 857 return True
851 858
852 859 def _down_pressed(self):
853 860 """ Called when the down key is pressed. Returns whether to continue
854 861 processing the event.
855 862 """
856 863 end_cursor = self._get_end_cursor()
857 864 if self.textCursor().blockNumber() == end_cursor.blockNumber():
858 865 self.history_next()
859 866 return False
860 867 return True
861 868
862 869 #---------------------------------------------------------------------------
863 870 # 'HistoryConsoleWidget' interface
864 871 #---------------------------------------------------------------------------
865 872
866 873 def history_previous(self):
867 874 """ If possible, set the input buffer to the previous item in the
868 875 history.
869 876 """
870 877 if self._history_index > 0:
871 878 self._history_index -= 1
872 879 self.input_buffer = self._history[self._history_index]
873 880
874 881 def history_next(self):
875 882 """ Set the input buffer to the next item in the history, or a blank
876 883 line if there is no subsequent item.
877 884 """
878 885 if self._history_index < len(self._history):
879 886 self._history_index += 1
880 887 if self._history_index < len(self._history):
881 888 self.input_buffer = self._history[self._history_index]
882 889 else:
883 890 self.input_buffer = ''
@@ -1,353 +1,357 b''
1 1 # Standard library imports
2 2 import signal
3 3
4 4 # System library imports
5 5 from pygments.lexers import PythonLexer
6 6 from PyQt4 import QtCore, QtGui
7 7 import zmq
8 8
9 9 # Local imports
10 10 from IPython.core.inputsplitter import InputSplitter
11 11 from call_tip_widget import CallTipWidget
12 12 from completion_lexer import CompletionLexer
13 13 from console_widget import HistoryConsoleWidget
14 14 from pygments_highlighter import PygmentsHighlighter
15 15
16 16
17 17 class FrontendHighlighter(PygmentsHighlighter):
18 18 """ A Python PygmentsHighlighter that can be turned on and off and which
19 19 knows about continuation prompts.
20 20 """
21 21
22 22 def __init__(self, frontend):
23 23 PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
24 24 self._current_offset = 0
25 25 self._frontend = frontend
26 26 self.highlighting_on = False
27 27
28 28 def highlightBlock(self, qstring):
29 29 """ Highlight a block of text. Reimplemented to highlight selectively.
30 30 """
31 31 if self.highlighting_on:
32 32 for prompt in (self._frontend._continuation_prompt,
33 33 self._frontend._prompt):
34 34 if qstring.startsWith(prompt):
35 35 qstring.remove(0, len(prompt))
36 36 self._current_offset = len(prompt)
37 37 break
38 38 PygmentsHighlighter.highlightBlock(self, qstring)
39 39
40 40 def setFormat(self, start, count, format):
41 41 """ Reimplemented to avoid highlighting continuation prompts.
42 42 """
43 43 start += self._current_offset
44 44 PygmentsHighlighter.setFormat(self, start, count, format)
45 45
46 46
47 47 class FrontendWidget(HistoryConsoleWidget):
48 48 """ A Qt frontend for a generic Python kernel.
49 49 """
50 50
51 51 # Emitted when an 'execute_reply' is received from the kernel.
52 52 executed = QtCore.pyqtSignal(object)
53 53
54 54 #---------------------------------------------------------------------------
55 55 # 'QObject' interface
56 56 #---------------------------------------------------------------------------
57 57
58 58 def __init__(self, parent=None):
59 59 super(FrontendWidget, self).__init__(parent)
60 60
61 61 # ConsoleWidget protected variables.
62 62 self._continuation_prompt = '... '
63 63
64 64 # FrontendWidget protected variables.
65 65 self._call_tip_widget = CallTipWidget(self)
66 66 self._completion_lexer = CompletionLexer(PythonLexer())
67 67 self._hidden = True
68 68 self._highlighter = FrontendHighlighter(self)
69 69 self._input_splitter = InputSplitter(input_mode='replace')
70 70 self._kernel_manager = None
71 71
72 72 self.document().contentsChange.connect(self._document_contents_change)
73 73
74 74 #---------------------------------------------------------------------------
75 75 # 'QWidget' interface
76 76 #---------------------------------------------------------------------------
77 77
78 78 def focusOutEvent(self, event):
79 79 """ Reimplemented to hide calltips.
80 80 """
81 81 self._call_tip_widget.hide()
82 82 super(FrontendWidget, self).focusOutEvent(event)
83 83
84 84 def keyPressEvent(self, event):
85 85 """ Reimplemented to allow calltips to process events and to send
86 86 signals to the kernel.
87 87 """
88 88 if self._executing and event.key() == QtCore.Qt.Key_C and \
89 89 self._control_down(event.modifiers()):
90 90 self._interrupt_kernel()
91 91 else:
92 92 if self._call_tip_widget.isVisible():
93 93 self._call_tip_widget.keyPressEvent(event)
94 94 super(FrontendWidget, self).keyPressEvent(event)
95 95
96 96 #---------------------------------------------------------------------------
97 97 # 'ConsoleWidget' abstract interface
98 98 #---------------------------------------------------------------------------
99 99
100 100 def _is_complete(self, source, interactive):
101 101 """ Returns whether 'source' can be completely processed and a new
102 102 prompt created. When triggered by an Enter/Return key press,
103 103 'interactive' is True; otherwise, it is False.
104 104 """
105 105 complete = self._input_splitter.push(source)
106 106 if interactive:
107 107 complete = not self._input_splitter.push_accepts_more()
108 108 return complete
109 109
110 110 def _execute(self, source, hidden):
111 111 """ Execute 'source'. If 'hidden', do not show any output.
112 112 """
113 113 self.kernel_manager.xreq_channel.execute(source)
114 114 self._hidden = hidden
115 115
116 116 def _prompt_started_hook(self):
117 117 """ Called immediately after a new prompt is displayed.
118 118 """
119 119 self._highlighter.highlighting_on = True
120 120
121 121 # Auto-indent if this is a continuation prompt.
122 122 if self._get_prompt_cursor().blockNumber() != \
123 123 self._get_end_cursor().blockNumber():
124 124 self.appendPlainText(' ' * self._input_splitter.indent_spaces)
125 125
126 126 def _prompt_finished_hook(self):
127 127 """ Called immediately after a prompt is finished, i.e. when some input
128 128 will be processed and a new prompt displayed.
129 129 """
130 130 self._highlighter.highlighting_on = False
131 131
132 132 def _tab_pressed(self):
133 133 """ Called when the tab key is pressed. Returns whether to continue
134 134 processing the event.
135 135 """
136 136 self._keep_cursor_in_buffer()
137 137 cursor = self.textCursor()
138 138 if not self._complete():
139 139 cursor.insertText(' ')
140 140 return False
141 141
142 142 #---------------------------------------------------------------------------
143 143 # 'ConsoleWidget' protected interface
144 144 #---------------------------------------------------------------------------
145 145
146 def _show_prompt(self, prompt=None):
146 def _show_prompt(self, prompt=None, newline=True):
147 147 """ Reimplemented to set a default prompt.
148 148 """
149 149 if prompt is None:
150 150 prompt = '>>> '
151 super(FrontendWidget, self)._show_prompt(prompt)
151 super(FrontendWidget, self)._show_prompt(prompt, newline)
152 152
153 153 #---------------------------------------------------------------------------
154 154 # 'FrontendWidget' interface
155 155 #---------------------------------------------------------------------------
156 156
157 157 def execute_file(self, path, hidden=False):
158 158 """ Attempts to execute file with 'path'. If 'hidden', no output is
159 159 shown.
160 160 """
161 161 self.execute('execfile("%s")' % path, hidden=hidden)
162 162
163 163 def _get_kernel_manager(self):
164 164 """ Returns the current kernel manager.
165 165 """
166 166 return self._kernel_manager
167 167
168 168 def _set_kernel_manager(self, kernel_manager):
169 169 """ Disconnect from the current kernel manager (if any) and set a new
170 170 kernel manager.
171 171 """
172 172 # Disconnect the old kernel manager, if necessary.
173 173 if self._kernel_manager is not None:
174 174 self._kernel_manager.started_channels.disconnect(
175 175 self._started_channels)
176 176 self._kernel_manager.stopped_channels.disconnect(
177 177 self._stopped_channels)
178 178
179 179 # Disconnect the old kernel manager's channels.
180 180 sub = self._kernel_manager.sub_channel
181 181 xreq = self._kernel_manager.xreq_channel
182 182 rep = self._kernel_manager.rep_channel
183 183 sub.message_received.disconnect(self._handle_sub)
184 184 xreq.execute_reply.disconnect(self._handle_execute_reply)
185 185 xreq.complete_reply.disconnect(self._handle_complete_reply)
186 186 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
187 187 rep.readline_requested.disconnect(self._handle_req)
188 188
189 189 # Handle the case where the old kernel manager is still listening.
190 190 if self._kernel_manager.channels_running:
191 191 self._stopped_channels()
192 192
193 193 # Set the new kernel manager.
194 194 self._kernel_manager = kernel_manager
195 195 if kernel_manager is None:
196 196 return
197 197
198 198 # Connect the new kernel manager.
199 199 kernel_manager.started_channels.connect(self._started_channels)
200 200 kernel_manager.stopped_channels.connect(self._stopped_channels)
201 201
202 202 # Connect the new kernel manager's channels.
203 203 sub = kernel_manager.sub_channel
204 204 xreq = kernel_manager.xreq_channel
205 205 rep = kernel_manager.rep_channel
206 206 sub.message_received.connect(self._handle_sub)
207 207 xreq.execute_reply.connect(self._handle_execute_reply)
208 208 xreq.complete_reply.connect(self._handle_complete_reply)
209 209 xreq.object_info_reply.connect(self._handle_object_info_reply)
210 210 rep.readline_requested.connect(self._handle_req)
211 211
212 212 # Handle the case where the kernel manager started channels before
213 213 # we connected.
214 214 if kernel_manager.channels_running:
215 215 self._started_channels()
216 216
217 217 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
218 218
219 219 #---------------------------------------------------------------------------
220 220 # 'FrontendWidget' protected interface
221 221 #---------------------------------------------------------------------------
222 222
223 223 def _call_tip(self):
224 224 """ Shows a call tip, if appropriate, at the current cursor location.
225 225 """
226 226 # Decide if it makes sense to show a call tip
227 227 cursor = self.textCursor()
228 228 cursor.movePosition(QtGui.QTextCursor.Left)
229 229 document = self.document()
230 230 if document.characterAt(cursor.position()).toAscii() != '(':
231 231 return False
232 232 context = self._get_context(cursor)
233 233 if not context:
234 234 return False
235 235
236 236 # Send the metadata request to the kernel
237 237 name = '.'.join(context)
238 238 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
239 239 self._calltip_pos = self.textCursor().position()
240 240 return True
241 241
242 242 def _complete(self):
243 243 """ Performs completion at the current cursor location.
244 244 """
245 245 # Decide if it makes sense to do completion
246 246 context = self._get_context()
247 247 if not context:
248 248 return False
249 249
250 250 # Send the completion request to the kernel
251 251 text = '.'.join(context)
252 252 self._complete_id = self.kernel_manager.xreq_channel.complete(
253 253 text, self.input_buffer_cursor_line, self.input_buffer)
254 254 self._complete_pos = self.textCursor().position()
255 255 return True
256 256
257 257 def _get_context(self, cursor=None):
258 258 """ Gets the context at the current cursor location.
259 259 """
260 260 if cursor is None:
261 261 cursor = self.textCursor()
262 262 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
263 263 QtGui.QTextCursor.KeepAnchor)
264 264 text = unicode(cursor.selectedText())
265 265 return self._completion_lexer.get_context(text)
266 266
267 267 def _interrupt_kernel(self):
268 268 """ Attempts to the interrupt the kernel.
269 269 """
270 270 if self.kernel_manager.has_kernel:
271 271 self.kernel_manager.signal_kernel(signal.SIGINT)
272 272 else:
273 273 self.appendPlainText('Kernel process is either remote or '
274 274 'unspecified. Cannot interrupt.\n')
275 275
276 276 #------ Signal handlers ----------------------------------------------------
277 277
278 278 def _started_channels(self):
279 279 """ Called when the kernel manager has started listening.
280 280 """
281 281 self.clear()
282 282
283 283 def _stopped_channels(self):
284 284 """ Called when the kernel manager has stopped listening.
285 285 """
286 286 pass
287 287
288 288 def _document_contents_change(self, position, removed, added):
289 289 """ Called whenever the document's content changes. Display a calltip
290 290 if appropriate.
291 291 """
292 292 # Calculate where the cursor should be *after* the change:
293 293 position += added
294 294
295 295 document = self.document()
296 296 if position == self.textCursor().position():
297 297 self._call_tip()
298 298
299 299 def _handle_req(self, req):
300 # Make sure that all output from the SUB channel has been processed
301 # before entering readline mode.
302 self.kernel_manager.sub_channel.flush()
303
300 304 def callback(line):
301 305 self.kernel_manager.rep_channel.readline(line)
302 306 self._readline(callback=callback)
303 307
304 308 def _handle_sub(self, omsg):
305 309 if self._hidden:
306 310 return
307 311 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
308 312 if handler is not None:
309 313 handler(omsg)
310 314
311 315 def _handle_pyout(self, omsg):
312 316 session = omsg['parent_header']['session']
313 317 if session == self.kernel_manager.session.session:
314 318 self.appendPlainText(omsg['content']['data'] + '\n')
315 319
316 320 def _handle_stream(self, omsg):
317 321 self.appendPlainText(omsg['content']['data'])
318 322 self.moveCursor(QtGui.QTextCursor.End)
319 323
320 324 def _handle_execute_reply(self, rep):
321 325 if self._hidden:
322 326 return
323 327
324 328 # Make sure that all output from the SUB channel has been processed
325 329 # before writing a new prompt.
326 330 self.kernel_manager.sub_channel.flush()
327 331
328 332 content = rep['content']
329 333 status = content['status']
330 334 if status == 'error':
331 335 self.appendPlainText(content['traceback'][-1])
332 336 elif status == 'aborted':
333 337 text = "ERROR: ABORTED\n"
334 338 self.appendPlainText(text)
335 339 self._hidden = True
336 340 self._show_prompt()
337 341 self.executed.emit(rep)
338 342
339 343 def _handle_complete_reply(self, rep):
340 344 cursor = self.textCursor()
341 345 if rep['parent_header']['msg_id'] == self._complete_id and \
342 346 cursor.position() == self._complete_pos:
343 347 text = '.'.join(self._get_context())
344 348 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
345 349 self._complete_with_items(cursor, rep['content']['matches'])
346 350
347 351 def _handle_object_info_reply(self, rep):
348 352 cursor = self.textCursor()
349 353 if rep['parent_header']['msg_id'] == self._calltip_id and \
350 354 cursor.position() == self._calltip_pos:
351 355 doc = rep['content']['docstring']
352 356 if doc:
353 357 self._call_tip_widget.show_docstring(doc)
@@ -1,452 +1,480 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 * Finish implementing `raw_input`.
7 7 * Implement `set_parent` logic. Right before doing exec, the Kernel should
8 8 call set_parent on all the PUB objects with the message about to be executed.
9 9 * Implement random port and security key logic.
10 10 * Implement control messages.
11 11 * Implement event loop and poll version.
12 12 """
13 13
14 14 # Standard library imports.
15 15 import __builtin__
16 from code import CommandCompiler
16 17 import os
17 18 import sys
18 19 import time
19 20 import traceback
20 from code import CommandCompiler
21 21
22 22 # System library imports.
23 23 import zmq
24 24
25 25 # Local imports.
26 26 from IPython.external.argparse import ArgumentParser
27 27 from session import Session, Message, extract_header
28 28 from completer import KernelCompleter
29 29
30 30
31 31 class InStream(object):
32 32 """ A file like object that reads from a 0MQ XREQ socket."""
33 33
34 34 def __init__(self, session, socket):
35 35 self.session = session
36 36 self.socket = socket
37 37
38 38 def close(self):
39 39 self.socket = None
40 40
41 41 def flush(self):
42 42 if self.socket is None:
43 raise ValueError(u'I/O operation on closed file')
43 raise ValueError('I/O operation on closed file')
44 44
45 45 def isatty(self):
46 46 return False
47 47
48 48 def next(self):
49 49 raise IOError('Seek not supported.')
50 50
51 51 def read(self, size=-1):
52 raise NotImplementedError
52 # FIXME: Do we want another request for this?
53 string = '\n'.join(self.readlines())
54 return self._truncate(string, size)
53 55
54 56 def readline(self, size=-1):
55 57 if self.socket is None:
56 raise ValueError(u'I/O operation on closed file')
58 raise ValueError('I/O operation on closed file')
57 59 else:
58 60 content = dict(size=size)
59 61 msg = self.session.msg('readline_request', content=content)
60 62 reply = self._request(msg)
61 return reply['content']['line']
63 line = reply['content']['line']
64 return self._truncate(line, size)
62 65
63 def readlines(self, size=-1):
64 raise NotImplementedError
66 def readlines(self, sizehint=-1):
67 # Sizehint is ignored, as is permitted.
68 if self.socket is None:
69 raise ValueError('I/O operation on closed file')
70 else:
71 lines = []
72 while True:
73 line = self.readline()
74 if line:
75 lines.append(line)
76 else:
77 break
78 return lines
65 79
66 80 def seek(self, offset, whence=None):
67 81 raise IOError('Seek not supported.')
68 82
69 83 def write(self, string):
70 84 raise IOError('Write not supported on a read only stream.')
71 85
72 86 def writelines(self, sequence):
73 87 raise IOError('Write not supported on a read only stream.')
74 88
75 89 def _request(self, msg):
90 # Flush output before making the request. This ensures, for example,
91 # that raw_input(prompt) actually gets a prompt written.
92 sys.stderr.flush()
93 sys.stdout.flush()
94
76 95 self.socket.send_json(msg)
77 96 while True:
78 97 try:
79 98 reply = self.socket.recv_json(zmq.NOBLOCK)
80 99 except zmq.ZMQError, e:
81 100 if e.errno == zmq.EAGAIN:
82 101 pass
83 102 else:
84 103 raise
85 104 else:
86 105 break
87 106 return reply
88 107
108 def _truncate(self, string, size):
109 if size >= 0:
110 if isinstance(string, str):
111 return string[:size]
112 elif isinstance(string, unicode):
113 encoded = string.encode('utf-8')[:size]
114 return encoded.decode('utf-8', 'ignore')
115 return string
116
89 117
90 118 class OutStream(object):
91 119 """A file like object that publishes the stream to a 0MQ PUB socket."""
92 120
93 121 def __init__(self, session, pub_socket, name, max_buffer=200):
94 122 self.session = session
95 123 self.pub_socket = pub_socket
96 124 self.name = name
97 125 self._buffer = []
98 126 self._buffer_len = 0
99 127 self.max_buffer = max_buffer
100 128 self.parent_header = {}
101 129
102 130 def set_parent(self, parent):
103 131 self.parent_header = extract_header(parent)
104 132
105 133 def close(self):
106 134 self.pub_socket = None
107 135
108 136 def flush(self):
109 137 if self.pub_socket is None:
110 138 raise ValueError(u'I/O operation on closed file')
111 139 else:
112 140 if self._buffer:
113 141 data = ''.join(self._buffer)
114 142 content = {u'name':self.name, u'data':data}
115 143 msg = self.session.msg(u'stream', content=content,
116 144 parent=self.parent_header)
117 145 print>>sys.__stdout__, Message(msg)
118 146 self.pub_socket.send_json(msg)
119 147 self._buffer_len = 0
120 148 self._buffer = []
121 149
122 150 def isatty(self):
123 151 return False
124 152
125 153 def next(self):
126 154 raise IOError('Read not supported on a write only stream.')
127 155
128 156 def read(self, size=None):
129 157 raise IOError('Read not supported on a write only stream.')
130 158
131 159 readline=read
132 160
133 161 def write(self, s):
134 162 if self.pub_socket is None:
135 163 raise ValueError('I/O operation on closed file')
136 164 else:
137 165 self._buffer.append(s)
138 166 self._buffer_len += len(s)
139 167 self._maybe_send()
140 168
141 169 def _maybe_send(self):
142 170 if '\n' in self._buffer[-1]:
143 171 self.flush()
144 172 if self._buffer_len > self.max_buffer:
145 173 self.flush()
146 174
147 175 def writelines(self, sequence):
148 176 if self.pub_socket is None:
149 177 raise ValueError('I/O operation on closed file')
150 178 else:
151 179 for s in sequence:
152 180 self.write(s)
153 181
154 182
155 183 class DisplayHook(object):
156 184
157 185 def __init__(self, session, pub_socket):
158 186 self.session = session
159 187 self.pub_socket = pub_socket
160 188 self.parent_header = {}
161 189
162 190 def __call__(self, obj):
163 191 if obj is None:
164 192 return
165 193
166 194 __builtin__._ = obj
167 195 msg = self.session.msg(u'pyout', {u'data':repr(obj)},
168 196 parent=self.parent_header)
169 197 self.pub_socket.send_json(msg)
170 198
171 199 def set_parent(self, parent):
172 200 self.parent_header = extract_header(parent)
173 201
174 202
175 203 class Kernel(object):
176 204
177 205 def __init__(self, session, reply_socket, pub_socket):
178 206 self.session = session
179 207 self.reply_socket = reply_socket
180 208 self.pub_socket = pub_socket
181 209 self.user_ns = {}
182 210 self.history = []
183 211 self.compiler = CommandCompiler()
184 212 self.completer = KernelCompleter(self.user_ns)
185 213 self.poll_ppid = False
186 214
187 215 # Build dict of handlers for message types
188 216 msg_types = [ 'execute_request', 'complete_request',
189 217 'object_info_request' ]
190 218 self.handlers = {}
191 219 for msg_type in msg_types:
192 220 self.handlers[msg_type] = getattr(self, msg_type)
193 221
194 222 def abort_queue(self):
195 223 while True:
196 224 try:
197 225 ident = self.reply_socket.recv(zmq.NOBLOCK)
198 226 except zmq.ZMQError, e:
199 227 if e.errno == zmq.EAGAIN:
200 228 break
201 229 else:
202 230 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
203 231 msg = self.reply_socket.recv_json()
204 232 print>>sys.__stdout__, "Aborting:"
205 233 print>>sys.__stdout__, Message(msg)
206 234 msg_type = msg['msg_type']
207 235 reply_type = msg_type.split('_')[0] + '_reply'
208 236 reply_msg = self.session.msg(reply_type, {'status' : 'aborted'}, msg)
209 237 print>>sys.__stdout__, Message(reply_msg)
210 238 self.reply_socket.send(ident,zmq.SNDMORE)
211 239 self.reply_socket.send_json(reply_msg)
212 240 # We need to wait a bit for requests to come in. This can probably
213 241 # be set shorter for true asynchronous clients.
214 242 time.sleep(0.1)
215 243
216 244 def execute_request(self, ident, parent):
217 245 try:
218 246 code = parent[u'content'][u'code']
219 247 except:
220 248 print>>sys.__stderr__, "Got bad msg: "
221 249 print>>sys.__stderr__, Message(parent)
222 250 return
223 251 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
224 252 self.pub_socket.send_json(pyin_msg)
225 253 try:
226 254 comp_code = self.compiler(code, '<zmq-kernel>')
227 255 sys.displayhook.set_parent(parent)
228 256 exec comp_code in self.user_ns, self.user_ns
229 257 except:
230 258 result = u'error'
231 259 etype, evalue, tb = sys.exc_info()
232 260 tb = traceback.format_exception(etype, evalue, tb)
233 261 exc_content = {
234 262 u'status' : u'error',
235 263 u'traceback' : tb,
236 264 u'etype' : unicode(etype),
237 265 u'evalue' : unicode(evalue)
238 266 }
239 267 exc_msg = self.session.msg(u'pyerr', exc_content, parent)
240 268 self.pub_socket.send_json(exc_msg)
241 269 reply_content = exc_content
242 270 else:
243 271 reply_content = {'status' : 'ok'}
244 272 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
245 273 print>>sys.__stdout__, Message(reply_msg)
246 274 self.reply_socket.send(ident, zmq.SNDMORE)
247 275 self.reply_socket.send_json(reply_msg)
248 276 if reply_msg['content']['status'] == u'error':
249 277 self.abort_queue()
250 278
251 279 def complete_request(self, ident, parent):
252 280 matches = {'matches' : self.complete(parent),
253 281 'status' : 'ok'}
254 282 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
255 283 matches, parent, ident)
256 284 print >> sys.__stdout__, completion_msg
257 285
258 286 def complete(self, msg):
259 287 return self.completer.complete(msg.content.line, msg.content.text)
260 288
261 289 def object_info_request(self, ident, parent):
262 290 context = parent['content']['oname'].split('.')
263 291 object_info = self.object_info(context)
264 292 msg = self.session.send(self.reply_socket, 'object_info_reply',
265 293 object_info, parent, ident)
266 294 print >> sys.__stdout__, msg
267 295
268 296 def object_info(self, context):
269 297 symbol, leftover = self.symbol_from_context(context)
270 298 if symbol is not None and not leftover:
271 299 doc = getattr(symbol, '__doc__', '')
272 300 else:
273 301 doc = ''
274 302 object_info = dict(docstring = doc)
275 303 return object_info
276 304
277 305 def symbol_from_context(self, context):
278 306 if not context:
279 307 return None, context
280 308
281 309 base_symbol_string = context[0]
282 310 symbol = self.user_ns.get(base_symbol_string, None)
283 311 if symbol is None:
284 312 symbol = __builtin__.__dict__.get(base_symbol_string, None)
285 313 if symbol is None:
286 314 return None, context
287 315
288 316 context = context[1:]
289 317 for i, name in enumerate(context):
290 318 new_symbol = getattr(symbol, name, None)
291 319 if new_symbol is None:
292 320 return symbol, context[i:]
293 321 else:
294 322 symbol = new_symbol
295 323
296 324 return symbol, []
297 325
298 326 def start(self):
299 327 while True:
300 328 if self.poll_ppid and os.getppid() == 1:
301 329 print>>sys.__stderr__, "KILLED KERNEL. No parent process."
302 330 os._exit(1)
303 331
304 332 ident = self.reply_socket.recv()
305 333 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
306 334 msg = self.reply_socket.recv_json()
307 335 omsg = Message(msg)
308 336 print>>sys.__stdout__
309 337 print>>sys.__stdout__, omsg
310 338 handler = self.handlers.get(omsg.msg_type, None)
311 339 if handler is None:
312 340 print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg
313 341 else:
314 342 handler(ident, omsg)
315 343
316 344
317 345 def bind_port(socket, ip, port):
318 346 """ Binds the specified ZMQ socket. If the port is less than zero, a random
319 347 port is chosen. Returns the port that was bound.
320 348 """
321 349 connection = 'tcp://%s' % ip
322 350 if port <= 0:
323 351 port = socket.bind_to_random_port(connection)
324 352 else:
325 353 connection += ':%i' % port
326 354 socket.bind(connection)
327 355 return port
328 356
329 357 def main():
330 358 """ Main entry point for launching a kernel.
331 359 """
332 360 # Parse command line arguments.
333 361 parser = ArgumentParser()
334 362 parser.add_argument('--ip', type=str, default='127.0.0.1',
335 363 help='set the kernel\'s IP address [default: local]')
336 364 parser.add_argument('--xrep', type=int, metavar='PORT', default=0,
337 365 help='set the XREP channel port [default: random]')
338 366 parser.add_argument('--pub', type=int, metavar='PORT', default=0,
339 367 help='set the PUB channel port [default: random]')
340 368 parser.add_argument('--req', type=int, metavar='PORT', default=0,
341 369 help='set the REQ channel port [default: random]')
342 370 parser.add_argument('--require-parent', action='store_true',
343 371 help='ensure that this process dies with its parent')
344 372 namespace = parser.parse_args()
345 373
346 374 # Create a context, a session, and the kernel sockets.
347 375 print >>sys.__stdout__, "Starting the kernel..."
348 376 context = zmq.Context()
349 377 session = Session(username=u'kernel')
350 378
351 379 reply_socket = context.socket(zmq.XREP)
352 380 xrep_port = bind_port(reply_socket, namespace.ip, namespace.xrep)
353 381 print >>sys.__stdout__, "XREP Channel on port", xrep_port
354 382
355 383 pub_socket = context.socket(zmq.PUB)
356 384 pub_port = bind_port(pub_socket, namespace.ip, namespace.pub)
357 385 print >>sys.__stdout__, "PUB Channel on port", pub_port
358 386
359 387 req_socket = context.socket(zmq.XREQ)
360 388 req_port = bind_port(req_socket, namespace.ip, namespace.req)
361 389 print >>sys.__stdout__, "REQ Channel on port", req_port
362 390
363 391 # Redirect input streams and set a display hook.
364 392 sys.stdin = InStream(session, req_socket)
365 393 sys.stdout = OutStream(session, pub_socket, u'stdout')
366 394 sys.stderr = OutStream(session, pub_socket, u'stderr')
367 395 sys.displayhook = DisplayHook(session, pub_socket)
368 396
369 397 # Create the kernel.
370 398 kernel = Kernel(session, reply_socket, pub_socket)
371 399
372 400 # Configure this kernel/process to die on parent termination, if necessary.
373 401 if namespace.require_parent:
374 402 if sys.platform == 'linux2':
375 403 import ctypes, ctypes.util, signal
376 404 PR_SET_PDEATHSIG = 1
377 405 libc = ctypes.CDLL(ctypes.util.find_library('c'))
378 406 libc.prctl(PR_SET_PDEATHSIG, signal.SIGKILL)
379 407
380 408 elif sys.platform != 'win32':
381 409 kernel.poll_ppid = True
382 410
383 411 # Start the kernel mainloop.
384 412 kernel.start()
385 413
386 414
387 415 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, independent=False):
388 416 """ Launches a localhost kernel, binding to the specified ports.
389 417
390 418 Parameters
391 419 ----------
392 420 xrep_port : int, optional
393 421 The port to use for XREP channel.
394 422
395 423 pub_port : int, optional
396 424 The port to use for the SUB channel.
397 425
398 426 req_port : int, optional
399 427 The port to use for the REQ (raw input) channel.
400 428
401 429 independent : bool, optional (default False)
402 430 If set, the kernel process is guaranteed to survive if this process
403 431 dies. If not set, an effort is made to ensure that the kernel is killed
404 432 when this process dies. Note that in this case it is still good practice
405 433 to attempt to kill kernels manually before exiting.
406 434
407 435 Returns
408 436 -------
409 437 A tuple of form:
410 438 (kernel_process, xrep_port, pub_port, req_port)
411 439 where kernel_process is a Popen object and the ports are integers.
412 440 """
413 441 import socket
414 442 from subprocess import Popen
415 443
416 444 # Find open ports as necessary.
417 445 ports = []
418 446 ports_needed = int(xrep_port <= 0) + int(pub_port <= 0) + int(req_port <= 0)
419 447 for i in xrange(ports_needed):
420 448 sock = socket.socket()
421 449 sock.bind(('', 0))
422 450 ports.append(sock)
423 451 for i, sock in enumerate(ports):
424 452 port = sock.getsockname()[1]
425 453 sock.close()
426 454 ports[i] = port
427 455 if xrep_port <= 0:
428 456 xrep_port = ports.pop(0)
429 457 if pub_port <= 0:
430 458 pub_port = ports.pop(0)
431 459 if req_port <= 0:
432 460 req_port = ports.pop(0)
433 461
434 462 # Spawn a kernel.
435 463 command = 'from IPython.zmq.kernel import main; main()'
436 464 arguments = [ sys.executable, '-c', command, '--xrep', str(xrep_port),
437 465 '--pub', str(pub_port), '--req', str(req_port) ]
438 466
439 467 if independent:
440 468 if sys.platform == 'win32':
441 469 proc = Popen(['start', '/b'] + arguments, shell=True)
442 470 else:
443 471 proc = Popen(arguments, preexec_fn=lambda: os.setsid())
444 472
445 473 else:
446 474 proc = Popen(arguments + ['--require-parent'])
447 475
448 476 return proc, xrep_port, pub_port, req_port
449 477
450 478
451 479 if __name__ == '__main__':
452 480 main()
General Comments 0
You need to be logged in to leave comments. Login now