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