##// END OF EJS Templates
First cut at allowing the kernel to be restarted from the frontend.
epatters -
Show More
@@ -1,1291 +1,1282 b''
1 1 # Standard library imports
2 2 import re
3 3 import sys
4 4 from textwrap import dedent
5 5
6 6 # System library imports
7 7 from PyQt4 import QtCore, QtGui
8 8
9 9 # Local imports
10 10 from ansi_code_processor import QtAnsiCodeProcessor
11 11 from completion_widget import CompletionWidget
12 12
13 13
14 14 class ConsoleWidget(QtGui.QWidget):
15 15 """ An abstract base class for console-type widgets. This class has
16 16 functionality for:
17 17
18 18 * Maintaining a prompt and editing region
19 19 * Providing the traditional Unix-style console keyboard shortcuts
20 20 * Performing tab completion
21 21 * Paging text
22 22 * Handling ANSI escape codes
23 23
24 24 ConsoleWidget also provides a number of utility methods that will be
25 25 convenient to implementors of a console-style widget.
26 26 """
27 27
28 28 # Whether to process ANSI escape codes.
29 29 ansi_codes = True
30 30
31 31 # The maximum number of lines of text before truncation.
32 32 buffer_size = 500
33 33
34 34 # Whether to use a list widget or plain text output for tab completion.
35 35 gui_completion = True
36 36
37 37 # Whether to override ShortcutEvents for the keybindings defined by this
38 38 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
39 39 # priority (when it has focus) over, e.g., window-level menu shortcuts.
40 40 override_shortcuts = False
41 41
42 42 # Signals that indicate ConsoleWidget state.
43 43 copy_available = QtCore.pyqtSignal(bool)
44 44 redo_available = QtCore.pyqtSignal(bool)
45 45 undo_available = QtCore.pyqtSignal(bool)
46 46
47 47 # Signal emitted when paging is needed and the paging style has been
48 48 # specified as 'custom'.
49 49 custom_page_requested = QtCore.pyqtSignal(object)
50 50
51 51 # Protected class variables.
52 52 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
53 53 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
54 54 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
55 55 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
56 56 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
57 57 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
58 58 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
59 59 _shortcuts = set(_ctrl_down_remap.keys() +
60 60 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
61 61
62 62 #---------------------------------------------------------------------------
63 63 # 'QObject' interface
64 64 #---------------------------------------------------------------------------
65 65
66 66 def __init__(self, kind='plain', paging='inside', parent=None):
67 67 """ Create a ConsoleWidget.
68 68
69 69 Parameters
70 70 ----------
71 71 kind : str, optional [default 'plain']
72 72 The type of underlying text widget to use. Valid values are 'plain',
73 73 which specifies a QPlainTextEdit, and 'rich', which specifies a
74 74 QTextEdit.
75 75
76 76 paging : str, optional [default 'inside']
77 77 The type of paging to use. Valid values are:
78 78 'inside' : The widget pages like a traditional terminal pager.
79 79 'hsplit' : When paging is requested, the widget is split
80 80 horizontally. The top pane contains the console,
81 81 and the bottom pane contains the paged text.
82 82 'vsplit' : Similar to 'hsplit', except that a vertical splitter
83 83 used.
84 84 'custom' : No action is taken by the widget beyond emitting a
85 85 'custom_page_requested(str)' signal.
86 86 'none' : The text is written directly to the console.
87 87
88 88 parent : QWidget, optional [default None]
89 89 The parent for this widget.
90 90 """
91 91 super(ConsoleWidget, self).__init__(parent)
92 92
93 93 # Create the layout and underlying text widget.
94 94 layout = QtGui.QStackedLayout(self)
95 95 layout.setMargin(0)
96 96 self._control = self._create_control(kind)
97 97 self._page_control = None
98 98 self._splitter = None
99 99 if paging in ('hsplit', 'vsplit'):
100 100 self._splitter = QtGui.QSplitter()
101 101 if paging == 'hsplit':
102 102 self._splitter.setOrientation(QtCore.Qt.Horizontal)
103 103 else:
104 104 self._splitter.setOrientation(QtCore.Qt.Vertical)
105 105 self._splitter.addWidget(self._control)
106 106 layout.addWidget(self._splitter)
107 107 else:
108 108 layout.addWidget(self._control)
109 109
110 110 # Create the paging widget, if necessary.
111 111 self._page_style = paging
112 112 if paging in ('inside', 'hsplit', 'vsplit'):
113 113 self._page_control = self._create_page_control()
114 114 if self._splitter:
115 115 self._page_control.hide()
116 116 self._splitter.addWidget(self._page_control)
117 117 else:
118 118 layout.addWidget(self._page_control)
119 119 elif paging not in ('custom', 'none'):
120 120 raise ValueError('Paging style %s unknown.' % repr(paging))
121 121
122 122 # Initialize protected variables. Some variables contain useful state
123 123 # information for subclasses; they should be considered read-only.
124 124 self._ansi_processor = QtAnsiCodeProcessor()
125 125 self._completion_widget = CompletionWidget(self._control)
126 126 self._continuation_prompt = '> '
127 127 self._continuation_prompt_html = None
128 128 self._executing = False
129 129 self._prompt = ''
130 130 self._prompt_html = None
131 131 self._prompt_pos = 0
132 132 self._reading = False
133 133 self._reading_callback = None
134 134 self._tab_width = 8
135 135
136 136 # Set a monospaced font.
137 137 self.reset_font()
138 138
139 139 def eventFilter(self, obj, event):
140 140 """ Reimplemented to ensure a console-like behavior in the underlying
141 141 text widget.
142 142 """
143 143 # Re-map keys for all filtered widgets.
144 144 etype = event.type()
145 145 if etype == QtCore.QEvent.KeyPress and \
146 146 self._control_key_down(event.modifiers()) and \
147 147 event.key() in self._ctrl_down_remap:
148 148 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
149 149 self._ctrl_down_remap[event.key()],
150 150 QtCore.Qt.NoModifier)
151 151 QtGui.qApp.sendEvent(obj, new_event)
152 152 return True
153 153
154 154 # Override shortucts for all filtered widgets. Note that on Mac OS it is
155 155 # always unnecessary to override shortcuts, hence the check below (users
156 156 # should just use the Control key instead of the Command key).
157 157 elif etype == QtCore.QEvent.ShortcutOverride and \
158 158 sys.platform != 'darwin' and \
159 159 self._control_key_down(event.modifiers()) and \
160 160 event.key() in self._shortcuts:
161 161 event.accept()
162 162 return False
163 163
164 164 elif obj == self._control:
165 165 # Disable moving text by drag and drop.
166 166 if etype == QtCore.QEvent.DragMove:
167 167 return True
168 168
169 169 elif etype == QtCore.QEvent.KeyPress:
170 170 return self._event_filter_console_keypress(event)
171 171
172 172 elif obj == self._page_control:
173 173 if etype == QtCore.QEvent.KeyPress:
174 174 return self._event_filter_page_keypress(event)
175 175
176 176 return super(ConsoleWidget, self).eventFilter(obj, event)
177 177
178 178 #---------------------------------------------------------------------------
179 179 # 'QWidget' interface
180 180 #---------------------------------------------------------------------------
181 181
182 182 def sizeHint(self):
183 183 """ Reimplemented to suggest a size that is 80 characters wide and
184 184 25 lines high.
185 185 """
186 186 style = self.style()
187 187 opt = QtGui.QStyleOptionHeader()
188 188 font_metrics = QtGui.QFontMetrics(self.font)
189 189 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
190 190
191 191 width = font_metrics.width(' ') * 80
192 192 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
193 193 if self._page_style == 'hsplit':
194 194 width = width * 2 + splitwidth
195 195
196 196 height = font_metrics.height() * 25
197 197 if self._page_style == 'vsplit':
198 198 height = height * 2 + splitwidth
199 199
200 200 return QtCore.QSize(width, height)
201 201
202 202 #---------------------------------------------------------------------------
203 203 # 'ConsoleWidget' public interface
204 204 #---------------------------------------------------------------------------
205 205
206 206 def can_paste(self):
207 207 """ Returns whether text can be pasted from the clipboard.
208 208 """
209 209 # Accept only text that can be ASCII encoded.
210 210 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
211 211 text = QtGui.QApplication.clipboard().text()
212 212 if not text.isEmpty():
213 213 try:
214 214 str(text)
215 215 return True
216 216 except UnicodeEncodeError:
217 217 pass
218 218 return False
219 219
220 220 def clear(self, keep_input=True):
221 221 """ Clear the console, then write a new prompt. If 'keep_input' is set,
222 222 restores the old input buffer when the new prompt is written.
223 223 """
224 224 if keep_input:
225 225 input_buffer = self.input_buffer
226 226 self._control.clear()
227 227 self._show_prompt()
228 228 if keep_input:
229 229 self.input_buffer = input_buffer
230 230
231 231 def copy(self):
232 232 """ Copy the current selected text to the clipboard.
233 233 """
234 234 self._control.copy()
235 235
236 236 def execute(self, source=None, hidden=False, interactive=False):
237 237 """ Executes source or the input buffer, possibly prompting for more
238 238 input.
239 239
240 240 Parameters:
241 241 -----------
242 242 source : str, optional
243 243
244 244 The source to execute. If not specified, the input buffer will be
245 245 used. If specified and 'hidden' is False, the input buffer will be
246 246 replaced with the source before execution.
247 247
248 248 hidden : bool, optional (default False)
249 249
250 250 If set, no output will be shown and the prompt will not be modified.
251 251 In other words, it will be completely invisible to the user that
252 252 an execution has occurred.
253 253
254 254 interactive : bool, optional (default False)
255 255
256 256 Whether the console is to treat the source as having been manually
257 257 entered by the user. The effect of this parameter depends on the
258 258 subclass implementation.
259 259
260 260 Raises:
261 261 -------
262 262 RuntimeError
263 263 If incomplete input is given and 'hidden' is True. In this case,
264 264 it is not possible to prompt for more input.
265 265
266 266 Returns:
267 267 --------
268 268 A boolean indicating whether the source was executed.
269 269 """
270 270 if not hidden:
271 271 if source is not None:
272 272 self.input_buffer = source
273 273
274 274 self._append_plain_text('\n')
275 275 self._executing_input_buffer = self.input_buffer
276 276 self._executing = True
277 277 self._prompt_finished()
278 278
279 279 real_source = self.input_buffer if source is None else source
280 280 complete = self._is_complete(real_source, interactive)
281 281 if complete:
282 282 if not hidden:
283 283 # The maximum block count is only in effect during execution.
284 284 # This ensures that _prompt_pos does not become invalid due to
285 285 # text truncation.
286 286 self._control.document().setMaximumBlockCount(self.buffer_size)
287 287 self._execute(real_source, hidden)
288 288 elif hidden:
289 289 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
290 290 else:
291 291 self._show_continuation_prompt()
292 292
293 293 return complete
294 294
295 295 def _get_input_buffer(self):
296 296 """ The text that the user has entered entered at the current prompt.
297 297 """
298 298 # If we're executing, the input buffer may not even exist anymore due to
299 299 # the limit imposed by 'buffer_size'. Therefore, we store it.
300 300 if self._executing:
301 301 return self._executing_input_buffer
302 302
303 303 cursor = self._get_end_cursor()
304 304 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
305 305 input_buffer = str(cursor.selection().toPlainText())
306 306
307 307 # Strip out continuation prompts.
308 308 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
309 309
310 310 def _set_input_buffer(self, string):
311 311 """ Replaces the text in the input buffer with 'string'.
312 312 """
313 313 # For now, it is an error to modify the input buffer during execution.
314 314 if self._executing:
315 315 raise RuntimeError("Cannot change input buffer during execution.")
316 316
317 317 # Remove old text.
318 318 cursor = self._get_end_cursor()
319 319 cursor.beginEditBlock()
320 320 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
321 321 cursor.removeSelectedText()
322 322
323 323 # Insert new text with continuation prompts.
324 324 lines = string.splitlines(True)
325 325 if lines:
326 326 self._append_plain_text(lines[0])
327 327 for i in xrange(1, len(lines)):
328 328 if self._continuation_prompt_html is None:
329 329 self._append_plain_text(self._continuation_prompt)
330 330 else:
331 331 self._append_html(self._continuation_prompt_html)
332 332 self._append_plain_text(lines[i])
333 333 cursor.endEditBlock()
334 334 self._control.moveCursor(QtGui.QTextCursor.End)
335 335
336 336 input_buffer = property(_get_input_buffer, _set_input_buffer)
337 337
338 338 def _get_font(self):
339 339 """ The base font being used by the ConsoleWidget.
340 340 """
341 341 return self._control.document().defaultFont()
342 342
343 343 def _set_font(self, font):
344 344 """ Sets the base font for the ConsoleWidget to the specified QFont.
345 345 """
346 346 font_metrics = QtGui.QFontMetrics(font)
347 347 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
348 348
349 349 self._completion_widget.setFont(font)
350 350 self._control.document().setDefaultFont(font)
351 351 if self._page_control:
352 352 self._page_control.document().setDefaultFont(font)
353 353
354 354 font = property(_get_font, _set_font)
355 355
356 356 def paste(self):
357 357 """ Paste the contents of the clipboard into the input region.
358 358 """
359 359 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
360 360 try:
361 361 text = str(QtGui.QApplication.clipboard().text())
362 362 except UnicodeEncodeError:
363 363 pass
364 364 else:
365 365 self._insert_plain_text_into_buffer(dedent(text))
366 366
367 367 def print_(self, printer):
368 368 """ Print the contents of the ConsoleWidget to the specified QPrinter.
369 369 """
370 370 self._control.print_(printer)
371 371
372 372 def redo(self):
373 373 """ Redo the last operation. If there is no operation to redo, nothing
374 374 happens.
375 375 """
376 376 self._control.redo()
377 377
378 378 def reset_font(self):
379 379 """ Sets the font to the default fixed-width font for this platform.
380 380 """
381 381 if sys.platform == 'win32':
382 382 name = 'Courier'
383 383 elif sys.platform == 'darwin':
384 384 name = 'Monaco'
385 385 else:
386 386 name = 'Monospace'
387 387 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
388 388 font.setStyleHint(QtGui.QFont.TypeWriter)
389 389 self._set_font(font)
390 390
391 391 def select_all(self):
392 392 """ Selects all the text in the buffer.
393 393 """
394 394 self._control.selectAll()
395 395
396 396 def _get_tab_width(self):
397 397 """ The width (in terms of space characters) for tab characters.
398 398 """
399 399 return self._tab_width
400 400
401 401 def _set_tab_width(self, tab_width):
402 402 """ Sets the width (in terms of space characters) for tab characters.
403 403 """
404 404 font_metrics = QtGui.QFontMetrics(self.font)
405 405 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
406 406
407 407 self._tab_width = tab_width
408 408
409 409 tab_width = property(_get_tab_width, _set_tab_width)
410 410
411 411 def undo(self):
412 412 """ Undo the last operation. If there is no operation to undo, nothing
413 413 happens.
414 414 """
415 415 self._control.undo()
416 416
417 417 #---------------------------------------------------------------------------
418 418 # 'ConsoleWidget' abstract interface
419 419 #---------------------------------------------------------------------------
420 420
421 421 def _is_complete(self, source, interactive):
422 422 """ Returns whether 'source' can be executed. When triggered by an
423 423 Enter/Return key press, 'interactive' is True; otherwise, it is
424 424 False.
425 425 """
426 426 raise NotImplementedError
427 427
428 428 def _execute(self, source, hidden):
429 429 """ Execute 'source'. If 'hidden', do not show any output.
430 430 """
431 431 raise NotImplementedError
432 432
433 def _execute_interrupt(self):
434 """ Attempts to stop execution. Returns whether this method has an
435 implementation.
436 """
437 return False
438
439 433 def _prompt_started_hook(self):
440 434 """ Called immediately after a new prompt is displayed.
441 435 """
442 436 pass
443 437
444 438 def _prompt_finished_hook(self):
445 439 """ Called immediately after a prompt is finished, i.e. when some input
446 440 will be processed and a new prompt displayed.
447 441 """
448 442 pass
449 443
450 444 def _up_pressed(self):
451 445 """ Called when the up key is pressed. Returns whether to continue
452 446 processing the event.
453 447 """
454 448 return True
455 449
456 450 def _down_pressed(self):
457 451 """ Called when the down key is pressed. Returns whether to continue
458 452 processing the event.
459 453 """
460 454 return True
461 455
462 456 def _tab_pressed(self):
463 457 """ Called when the tab key is pressed. Returns whether to continue
464 458 processing the event.
465 459 """
466 460 return False
467 461
468 462 #--------------------------------------------------------------------------
469 463 # 'ConsoleWidget' protected interface
470 464 #--------------------------------------------------------------------------
471 465
472 466 def _append_html(self, html):
473 467 """ Appends html at the end of the console buffer.
474 468 """
475 469 cursor = self._get_end_cursor()
476 470 self._insert_html(cursor, html)
477 471
478 472 def _append_html_fetching_plain_text(self, html):
479 473 """ Appends 'html', then returns the plain text version of it.
480 474 """
481 475 cursor = self._get_end_cursor()
482 476 return self._insert_html_fetching_plain_text(cursor, html)
483 477
484 478 def _append_plain_text(self, text):
485 479 """ Appends plain text at the end of the console buffer, processing
486 480 ANSI codes if enabled.
487 481 """
488 482 cursor = self._get_end_cursor()
489 483 self._insert_plain_text(cursor, text)
490 484
491 485 def _append_plain_text_keeping_prompt(self, text):
492 486 """ Writes 'text' after the current prompt, then restores the old prompt
493 487 with its old input buffer.
494 488 """
495 489 input_buffer = self.input_buffer
496 490 self._append_plain_text('\n')
497 491 self._prompt_finished()
498 492
499 493 self._append_plain_text(text)
500 494 self._show_prompt()
501 495 self.input_buffer = input_buffer
502 496
503 497 def _complete_with_items(self, cursor, items):
504 498 """ Performs completion with 'items' at the specified cursor location.
505 499 """
506 500 if len(items) == 1:
507 501 cursor.setPosition(self._control.textCursor().position(),
508 502 QtGui.QTextCursor.KeepAnchor)
509 503 cursor.insertText(items[0])
510 504 elif len(items) > 1:
511 505 if self.gui_completion:
512 506 self._completion_widget.show_items(cursor, items)
513 507 else:
514 508 text = self._format_as_columns(items)
515 509 self._append_plain_text_keeping_prompt(text)
516 510
517 511 def _control_key_down(self, modifiers):
518 512 """ Given a KeyboardModifiers flags object, return whether the Control
519 513 key is down (on Mac OS, treat the Command key as a synonym for
520 514 Control).
521 515 """
522 516 down = bool(modifiers & QtCore.Qt.ControlModifier)
523 517
524 518 # Note: on Mac OS, ControlModifier corresponds to the Command key while
525 519 # MetaModifier corresponds to the Control key.
526 520 if sys.platform == 'darwin':
527 521 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
528 522
529 523 return down
530 524
531 525 def _create_control(self, kind):
532 526 """ Creates and connects the underlying text widget.
533 527 """
534 528 if kind == 'plain':
535 529 control = QtGui.QPlainTextEdit()
536 530 elif kind == 'rich':
537 531 control = QtGui.QTextEdit()
538 532 control.setAcceptRichText(False)
539 533 else:
540 534 raise ValueError("Kind %s unknown." % repr(kind))
541 535 control.installEventFilter(self)
542 536 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
543 537 control.customContextMenuRequested.connect(self._show_context_menu)
544 538 control.copyAvailable.connect(self.copy_available)
545 539 control.redoAvailable.connect(self.redo_available)
546 540 control.undoAvailable.connect(self.undo_available)
547 541 control.setReadOnly(True)
548 542 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
549 543 return control
550 544
551 545 def _create_page_control(self):
552 546 """ Creates and connects the underlying paging widget.
553 547 """
554 548 control = QtGui.QPlainTextEdit()
555 549 control.installEventFilter(self)
556 550 control.setReadOnly(True)
557 551 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
558 552 return control
559 553
560 554 def _event_filter_console_keypress(self, event):
561 555 """ Filter key events for the underlying text widget to create a
562 556 console-like interface.
563 557 """
564 558 intercepted = False
565 559 cursor = self._control.textCursor()
566 560 position = cursor.position()
567 561 key = event.key()
568 562 ctrl_down = self._control_key_down(event.modifiers())
569 563 alt_down = event.modifiers() & QtCore.Qt.AltModifier
570 564 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
571 565
572 566 if event.matches(QtGui.QKeySequence.Paste):
573 567 # Call our paste instead of the underlying text widget's.
574 568 self.paste()
575 569 intercepted = True
576 570
577 571 elif ctrl_down:
578 if key == QtCore.Qt.Key_C:
579 intercepted = self._executing and self._execute_interrupt()
580
581 elif key == QtCore.Qt.Key_K:
572 if key == QtCore.Qt.Key_K:
582 573 if self._in_buffer(position):
583 574 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
584 575 QtGui.QTextCursor.KeepAnchor)
585 576 cursor.removeSelectedText()
586 577 intercepted = True
587 578
588 579 elif key == QtCore.Qt.Key_X:
589 580 intercepted = True
590 581
591 582 elif key == QtCore.Qt.Key_Y:
592 583 self.paste()
593 584 intercepted = True
594 585
595 586 elif alt_down:
596 587 if key == QtCore.Qt.Key_B:
597 588 self._set_cursor(self._get_word_start_cursor(position))
598 589 intercepted = True
599 590
600 591 elif key == QtCore.Qt.Key_F:
601 592 self._set_cursor(self._get_word_end_cursor(position))
602 593 intercepted = True
603 594
604 595 elif key == QtCore.Qt.Key_Backspace:
605 596 cursor = self._get_word_start_cursor(position)
606 597 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
607 598 cursor.removeSelectedText()
608 599 intercepted = True
609 600
610 601 elif key == QtCore.Qt.Key_D:
611 602 cursor = self._get_word_end_cursor(position)
612 603 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
613 604 cursor.removeSelectedText()
614 605 intercepted = True
615 606
616 607 else:
617 608 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
618 609 if self._reading:
619 610 self._append_plain_text('\n')
620 611 self._reading = False
621 612 if self._reading_callback:
622 613 self._reading_callback()
623 614 elif not self._executing:
624 615 self.execute(interactive=True)
625 616 intercepted = True
626 617
627 618 elif key == QtCore.Qt.Key_Up:
628 619 if self._reading or not self._up_pressed():
629 620 intercepted = True
630 621 else:
631 622 prompt_line = self._get_prompt_cursor().blockNumber()
632 623 intercepted = cursor.blockNumber() <= prompt_line
633 624
634 625 elif key == QtCore.Qt.Key_Down:
635 626 if self._reading or not self._down_pressed():
636 627 intercepted = True
637 628 else:
638 629 end_line = self._get_end_cursor().blockNumber()
639 630 intercepted = cursor.blockNumber() == end_line
640 631
641 632 elif key == QtCore.Qt.Key_Tab:
642 633 if not self._reading:
643 634 intercepted = not self._tab_pressed()
644 635
645 636 elif key == QtCore.Qt.Key_Left:
646 637 intercepted = not self._in_buffer(position - 1)
647 638
648 639 elif key == QtCore.Qt.Key_Home:
649 640 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
650 641 start_line = cursor.blockNumber()
651 642 if start_line == self._get_prompt_cursor().blockNumber():
652 643 start_pos = self._prompt_pos
653 644 else:
654 645 start_pos = cursor.position()
655 646 start_pos += len(self._continuation_prompt)
656 647 if shift_down and self._in_buffer(position):
657 648 self._set_selection(position, start_pos)
658 649 else:
659 650 self._set_position(start_pos)
660 651 intercepted = True
661 652
662 653 elif key == QtCore.Qt.Key_Backspace:
663 654
664 655 # Line deletion (remove continuation prompt)
665 656 len_prompt = len(self._continuation_prompt)
666 657 if not self._reading and \
667 658 cursor.columnNumber() == len_prompt and \
668 659 position != self._prompt_pos:
669 660 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
670 661 QtGui.QTextCursor.KeepAnchor)
671 662 cursor.removeSelectedText()
672 663 cursor.deletePreviousChar()
673 664 intercepted = True
674 665
675 666 # Regular backwards deletion
676 667 else:
677 668 anchor = cursor.anchor()
678 669 if anchor == position:
679 670 intercepted = not self._in_buffer(position - 1)
680 671 else:
681 672 intercepted = not self._in_buffer(min(anchor, position))
682 673
683 674 elif key == QtCore.Qt.Key_Delete:
684 675 anchor = cursor.anchor()
685 676 intercepted = not self._in_buffer(min(anchor, position))
686 677
687 678 # Don't move the cursor if control is down to allow copy-paste using
688 679 # the keyboard in any part of the buffer.
689 680 if not ctrl_down:
690 681 self._keep_cursor_in_buffer()
691 682
692 683 return intercepted
693 684
694 685 def _event_filter_page_keypress(self, event):
695 686 """ Filter key events for the paging widget to create console-like
696 687 interface.
697 688 """
698 689 key = event.key()
699 690
700 691 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
701 692 if self._splitter:
702 693 self._page_control.hide()
703 694 else:
704 695 self.layout().setCurrentWidget(self._control)
705 696 return True
706 697
707 698 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
708 699 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
709 700 QtCore.Qt.Key_Down,
710 701 QtCore.Qt.NoModifier)
711 702 QtGui.qApp.sendEvent(self._page_control, new_event)
712 703 return True
713 704
714 705 return False
715 706
716 707 def _format_as_columns(self, items, separator=' '):
717 708 """ Transform a list of strings into a single string with columns.
718 709
719 710 Parameters
720 711 ----------
721 712 items : sequence of strings
722 713 The strings to process.
723 714
724 715 separator : str, optional [default is two spaces]
725 716 The string that separates columns.
726 717
727 718 Returns
728 719 -------
729 720 The formatted string.
730 721 """
731 722 # Note: this code is adapted from columnize 0.3.2.
732 723 # See http://code.google.com/p/pycolumnize/
733 724
734 725 width = self._control.viewport().width()
735 726 char_width = QtGui.QFontMetrics(self.font).width(' ')
736 727 displaywidth = max(5, width / char_width)
737 728
738 729 # Some degenerate cases.
739 730 size = len(items)
740 731 if size == 0:
741 732 return '\n'
742 733 elif size == 1:
743 734 return '%s\n' % str(items[0])
744 735
745 736 # Try every row count from 1 upwards
746 737 array_index = lambda nrows, row, col: nrows*col + row
747 738 for nrows in range(1, size):
748 739 ncols = (size + nrows - 1) // nrows
749 740 colwidths = []
750 741 totwidth = -len(separator)
751 742 for col in range(ncols):
752 743 # Get max column width for this column
753 744 colwidth = 0
754 745 for row in range(nrows):
755 746 i = array_index(nrows, row, col)
756 747 if i >= size: break
757 748 x = items[i]
758 749 colwidth = max(colwidth, len(x))
759 750 colwidths.append(colwidth)
760 751 totwidth += colwidth + len(separator)
761 752 if totwidth > displaywidth:
762 753 break
763 754 if totwidth <= displaywidth:
764 755 break
765 756
766 757 # The smallest number of rows computed and the max widths for each
767 758 # column has been obtained. Now we just have to format each of the rows.
768 759 string = ''
769 760 for row in range(nrows):
770 761 texts = []
771 762 for col in range(ncols):
772 763 i = row + nrows*col
773 764 if i >= size:
774 765 texts.append('')
775 766 else:
776 767 texts.append(items[i])
777 768 while texts and not texts[-1]:
778 769 del texts[-1]
779 770 for col in range(len(texts)):
780 771 texts[col] = texts[col].ljust(colwidths[col])
781 772 string += '%s\n' % str(separator.join(texts))
782 773 return string
783 774
784 775 def _get_block_plain_text(self, block):
785 776 """ Given a QTextBlock, return its unformatted text.
786 777 """
787 778 cursor = QtGui.QTextCursor(block)
788 779 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
789 780 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
790 781 QtGui.QTextCursor.KeepAnchor)
791 782 return str(cursor.selection().toPlainText())
792 783
793 784 def _get_cursor(self):
794 785 """ Convenience method that returns a cursor for the current position.
795 786 """
796 787 return self._control.textCursor()
797 788
798 789 def _get_end_cursor(self):
799 790 """ Convenience method that returns a cursor for the last character.
800 791 """
801 792 cursor = self._control.textCursor()
802 793 cursor.movePosition(QtGui.QTextCursor.End)
803 794 return cursor
804 795
805 796 def _get_input_buffer_cursor_column(self):
806 797 """ Returns the column of the cursor in the input buffer, excluding the
807 798 contribution by the prompt, or -1 if there is no such column.
808 799 """
809 800 prompt = self._get_input_buffer_cursor_prompt()
810 801 if prompt is None:
811 802 return -1
812 803 else:
813 804 cursor = self._control.textCursor()
814 805 return cursor.columnNumber() - len(prompt)
815 806
816 807 def _get_input_buffer_cursor_line(self):
817 808 """ Returns line of the input buffer that contains the cursor, or None
818 809 if there is no such line.
819 810 """
820 811 prompt = self._get_input_buffer_cursor_prompt()
821 812 if prompt is None:
822 813 return None
823 814 else:
824 815 cursor = self._control.textCursor()
825 816 text = self._get_block_plain_text(cursor.block())
826 817 return text[len(prompt):]
827 818
828 819 def _get_input_buffer_cursor_prompt(self):
829 820 """ Returns the (plain text) prompt for line of the input buffer that
830 821 contains the cursor, or None if there is no such line.
831 822 """
832 823 if self._executing:
833 824 return None
834 825 cursor = self._control.textCursor()
835 826 if cursor.position() >= self._prompt_pos:
836 827 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
837 828 return self._prompt
838 829 else:
839 830 return self._continuation_prompt
840 831 else:
841 832 return None
842 833
843 834 def _get_prompt_cursor(self):
844 835 """ Convenience method that returns a cursor for the prompt position.
845 836 """
846 837 cursor = self._control.textCursor()
847 838 cursor.setPosition(self._prompt_pos)
848 839 return cursor
849 840
850 841 def _get_selection_cursor(self, start, end):
851 842 """ Convenience method that returns a cursor with text selected between
852 843 the positions 'start' and 'end'.
853 844 """
854 845 cursor = self._control.textCursor()
855 846 cursor.setPosition(start)
856 847 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
857 848 return cursor
858 849
859 850 def _get_word_start_cursor(self, position):
860 851 """ Find the start of the word to the left the given position. If a
861 852 sequence of non-word characters precedes the first word, skip over
862 853 them. (This emulates the behavior of bash, emacs, etc.)
863 854 """
864 855 document = self._control.document()
865 856 position -= 1
866 857 while position >= self._prompt_pos and \
867 858 not document.characterAt(position).isLetterOrNumber():
868 859 position -= 1
869 860 while position >= self._prompt_pos and \
870 861 document.characterAt(position).isLetterOrNumber():
871 862 position -= 1
872 863 cursor = self._control.textCursor()
873 864 cursor.setPosition(position + 1)
874 865 return cursor
875 866
876 867 def _get_word_end_cursor(self, position):
877 868 """ Find the end of the word to the right the given position. If a
878 869 sequence of non-word characters precedes the first word, skip over
879 870 them. (This emulates the behavior of bash, emacs, etc.)
880 871 """
881 872 document = self._control.document()
882 873 end = self._get_end_cursor().position()
883 874 while position < end and \
884 875 not document.characterAt(position).isLetterOrNumber():
885 876 position += 1
886 877 while position < end and \
887 878 document.characterAt(position).isLetterOrNumber():
888 879 position += 1
889 880 cursor = self._control.textCursor()
890 881 cursor.setPosition(position)
891 882 return cursor
892 883
893 884 def _insert_html(self, cursor, html):
894 885 """ Inserts HTML using the specified cursor in such a way that future
895 886 formatting is unaffected.
896 887 """
897 888 cursor.beginEditBlock()
898 889 cursor.insertHtml(html)
899 890
900 891 # After inserting HTML, the text document "remembers" it's in "html
901 892 # mode", which means that subsequent calls adding plain text will result
902 893 # in unwanted formatting, lost tab characters, etc. The following code
903 894 # hacks around this behavior, which I consider to be a bug in Qt, by
904 895 # (crudely) resetting the document's style state.
905 896 cursor.movePosition(QtGui.QTextCursor.Left,
906 897 QtGui.QTextCursor.KeepAnchor)
907 898 if cursor.selection().toPlainText() == ' ':
908 899 cursor.removeSelectedText()
909 900 else:
910 901 cursor.movePosition(QtGui.QTextCursor.Right)
911 902 cursor.insertText(' ', QtGui.QTextCharFormat())
912 903 cursor.endEditBlock()
913 904
914 905 def _insert_html_fetching_plain_text(self, cursor, html):
915 906 """ Inserts HTML using the specified cursor, then returns its plain text
916 907 version.
917 908 """
918 909 cursor.beginEditBlock()
919 910 cursor.removeSelectedText()
920 911
921 912 start = cursor.position()
922 913 self._insert_html(cursor, html)
923 914 end = cursor.position()
924 915 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
925 916 text = str(cursor.selection().toPlainText())
926 917
927 918 cursor.setPosition(end)
928 919 cursor.endEditBlock()
929 920 return text
930 921
931 922 def _insert_plain_text(self, cursor, text):
932 923 """ Inserts plain text using the specified cursor, processing ANSI codes
933 924 if enabled.
934 925 """
935 926 cursor.beginEditBlock()
936 927 if self.ansi_codes:
937 928 for substring in self._ansi_processor.split_string(text):
938 929 for action in self._ansi_processor.actions:
939 930 if action.kind == 'erase' and action.area == 'screen':
940 931 cursor.select(QtGui.QTextCursor.Document)
941 932 cursor.removeSelectedText()
942 933 format = self._ansi_processor.get_format()
943 934 cursor.insertText(substring, format)
944 935 else:
945 936 cursor.insertText(text)
946 937 cursor.endEditBlock()
947 938
948 939 def _insert_plain_text_into_buffer(self, text):
949 940 """ Inserts text into the input buffer at the current cursor position,
950 941 ensuring that continuation prompts are inserted as necessary.
951 942 """
952 943 lines = str(text).splitlines(True)
953 944 if lines:
954 945 self._keep_cursor_in_buffer()
955 946 cursor = self._control.textCursor()
956 947 cursor.beginEditBlock()
957 948 cursor.insertText(lines[0])
958 949 for line in lines[1:]:
959 950 if self._continuation_prompt_html is None:
960 951 cursor.insertText(self._continuation_prompt)
961 952 else:
962 953 self._continuation_prompt = \
963 954 self._insert_html_fetching_plain_text(
964 955 cursor, self._continuation_prompt_html)
965 956 cursor.insertText(line)
966 957 cursor.endEditBlock()
967 958 self._control.setTextCursor(cursor)
968 959
969 960 def _in_buffer(self, position=None):
970 961 """ Returns whether the current cursor (or, if specified, a position) is
971 962 inside the editing region.
972 963 """
973 964 cursor = self._control.textCursor()
974 965 if position is None:
975 966 position = cursor.position()
976 967 else:
977 968 cursor.setPosition(position)
978 969 line = cursor.blockNumber()
979 970 prompt_line = self._get_prompt_cursor().blockNumber()
980 971 if line == prompt_line:
981 972 return position >= self._prompt_pos
982 973 elif line > prompt_line:
983 974 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
984 975 prompt_pos = cursor.position() + len(self._continuation_prompt)
985 976 return position >= prompt_pos
986 977 return False
987 978
988 979 def _keep_cursor_in_buffer(self):
989 980 """ Ensures that the cursor is inside the editing region. Returns
990 981 whether the cursor was moved.
991 982 """
992 983 moved = not self._in_buffer()
993 984 if moved:
994 985 cursor = self._control.textCursor()
995 986 cursor.movePosition(QtGui.QTextCursor.End)
996 987 self._control.setTextCursor(cursor)
997 988 return moved
998 989
999 990 def _page(self, text):
1000 991 """ Displays text using the pager if it exceeds the height of the
1001 992 visible area.
1002 993 """
1003 994 if self._page_style == 'none':
1004 995 self._append_plain_text(text)
1005 996 else:
1006 997 line_height = QtGui.QFontMetrics(self.font).height()
1007 998 minlines = self._control.viewport().height() / line_height
1008 999 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1009 1000 if self._page_style == 'custom':
1010 1001 self.custom_page_requested.emit(text)
1011 1002 else:
1012 1003 self._page_control.clear()
1013 1004 cursor = self._page_control.textCursor()
1014 1005 self._insert_plain_text(cursor, text)
1015 1006 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1016 1007
1017 1008 self._page_control.viewport().resize(self._control.size())
1018 1009 if self._splitter:
1019 1010 self._page_control.show()
1020 1011 self._page_control.setFocus()
1021 1012 else:
1022 1013 self.layout().setCurrentWidget(self._page_control)
1023 1014 else:
1024 1015 self._append_plain_text(text)
1025 1016
1026 1017 def _prompt_started(self):
1027 1018 """ Called immediately after a new prompt is displayed.
1028 1019 """
1029 1020 # Temporarily disable the maximum block count to permit undo/redo and
1030 1021 # to ensure that the prompt position does not change due to truncation.
1031 1022 self._control.document().setMaximumBlockCount(0)
1032 1023 self._control.setUndoRedoEnabled(True)
1033 1024
1034 1025 self._control.setReadOnly(False)
1035 1026 self._control.moveCursor(QtGui.QTextCursor.End)
1036 1027
1037 1028 self._executing = False
1038 1029 self._prompt_started_hook()
1039 1030
1040 1031 def _prompt_finished(self):
1041 1032 """ Called immediately after a prompt is finished, i.e. when some input
1042 1033 will be processed and a new prompt displayed.
1043 1034 """
1044 1035 self._control.setUndoRedoEnabled(False)
1045 1036 self._control.setReadOnly(True)
1046 1037 self._prompt_finished_hook()
1047 1038
1048 1039 def _readline(self, prompt='', callback=None):
1049 1040 """ Reads one line of input from the user.
1050 1041
1051 1042 Parameters
1052 1043 ----------
1053 1044 prompt : str, optional
1054 1045 The prompt to print before reading the line.
1055 1046
1056 1047 callback : callable, optional
1057 1048 A callback to execute with the read line. If not specified, input is
1058 1049 read *synchronously* and this method does not return until it has
1059 1050 been read.
1060 1051
1061 1052 Returns
1062 1053 -------
1063 1054 If a callback is specified, returns nothing. Otherwise, returns the
1064 1055 input string with the trailing newline stripped.
1065 1056 """
1066 1057 if self._reading:
1067 1058 raise RuntimeError('Cannot read a line. Widget is already reading.')
1068 1059
1069 1060 if not callback and not self.isVisible():
1070 1061 # If the user cannot see the widget, this function cannot return.
1071 1062 raise RuntimeError('Cannot synchronously read a line if the widget'
1072 1063 'is not visible!')
1073 1064
1074 1065 self._reading = True
1075 1066 self._show_prompt(prompt, newline=False)
1076 1067
1077 1068 if callback is None:
1078 1069 self._reading_callback = None
1079 1070 while self._reading:
1080 1071 QtCore.QCoreApplication.processEvents()
1081 1072 return self.input_buffer.rstrip('\n')
1082 1073
1083 1074 else:
1084 1075 self._reading_callback = lambda: \
1085 1076 callback(self.input_buffer.rstrip('\n'))
1086 1077
1087 1078 def _set_continuation_prompt(self, prompt, html=False):
1088 1079 """ Sets the continuation prompt.
1089 1080
1090 1081 Parameters
1091 1082 ----------
1092 1083 prompt : str
1093 1084 The prompt to show when more input is needed.
1094 1085
1095 1086 html : bool, optional (default False)
1096 1087 If set, the prompt will be inserted as formatted HTML. Otherwise,
1097 1088 the prompt will be treated as plain text, though ANSI color codes
1098 1089 will be handled.
1099 1090 """
1100 1091 if html:
1101 1092 self._continuation_prompt_html = prompt
1102 1093 else:
1103 1094 self._continuation_prompt = prompt
1104 1095 self._continuation_prompt_html = None
1105 1096
1106 1097 def _set_cursor(self, cursor):
1107 1098 """ Convenience method to set the current cursor.
1108 1099 """
1109 1100 self._control.setTextCursor(cursor)
1110 1101
1111 1102 def _set_position(self, position):
1112 1103 """ Convenience method to set the position of the cursor.
1113 1104 """
1114 1105 cursor = self._control.textCursor()
1115 1106 cursor.setPosition(position)
1116 1107 self._control.setTextCursor(cursor)
1117 1108
1118 1109 def _set_selection(self, start, end):
1119 1110 """ Convenience method to set the current selected text.
1120 1111 """
1121 1112 self._control.setTextCursor(self._get_selection_cursor(start, end))
1122 1113
1123 1114 def _show_context_menu(self, pos):
1124 1115 """ Shows a context menu at the given QPoint (in widget coordinates).
1125 1116 """
1126 1117 menu = QtGui.QMenu()
1127 1118
1128 1119 copy_action = menu.addAction('Copy', self.copy)
1129 1120 copy_action.setEnabled(self._get_cursor().hasSelection())
1130 1121 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1131 1122
1132 1123 paste_action = menu.addAction('Paste', self.paste)
1133 1124 paste_action.setEnabled(self.can_paste())
1134 1125 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1135 1126
1136 1127 menu.addSeparator()
1137 1128 menu.addAction('Select All', self.select_all)
1138 1129
1139 1130 menu.exec_(self._control.mapToGlobal(pos))
1140 1131
1141 1132 def _show_prompt(self, prompt=None, html=False, newline=True):
1142 1133 """ Writes a new prompt at the end of the buffer.
1143 1134
1144 1135 Parameters
1145 1136 ----------
1146 1137 prompt : str, optional
1147 1138 The prompt to show. If not specified, the previous prompt is used.
1148 1139
1149 1140 html : bool, optional (default False)
1150 1141 Only relevant when a prompt is specified. If set, the prompt will
1151 1142 be inserted as formatted HTML. Otherwise, the prompt will be treated
1152 1143 as plain text, though ANSI color codes will be handled.
1153 1144
1154 1145 newline : bool, optional (default True)
1155 1146 If set, a new line will be written before showing the prompt if
1156 1147 there is not already a newline at the end of the buffer.
1157 1148 """
1158 1149 # Insert a preliminary newline, if necessary.
1159 1150 if newline:
1160 1151 cursor = self._get_end_cursor()
1161 1152 if cursor.position() > 0:
1162 1153 cursor.movePosition(QtGui.QTextCursor.Left,
1163 1154 QtGui.QTextCursor.KeepAnchor)
1164 1155 if str(cursor.selection().toPlainText()) != '\n':
1165 1156 self._append_plain_text('\n')
1166 1157
1167 1158 # Write the prompt.
1168 1159 if prompt is None:
1169 1160 if self._prompt_html is None:
1170 1161 self._append_plain_text(self._prompt)
1171 1162 else:
1172 1163 self._append_html(self._prompt_html)
1173 1164 else:
1174 1165 if html:
1175 1166 self._prompt = self._append_html_fetching_plain_text(prompt)
1176 1167 self._prompt_html = prompt
1177 1168 else:
1178 1169 self._append_plain_text(prompt)
1179 1170 self._prompt = prompt
1180 1171 self._prompt_html = None
1181 1172
1182 1173 self._prompt_pos = self._get_end_cursor().position()
1183 1174 self._prompt_started()
1184 1175
1185 1176 def _show_continuation_prompt(self):
1186 1177 """ Writes a new continuation prompt at the end of the buffer.
1187 1178 """
1188 1179 if self._continuation_prompt_html is None:
1189 1180 self._append_plain_text(self._continuation_prompt)
1190 1181 else:
1191 1182 self._continuation_prompt = self._append_html_fetching_plain_text(
1192 1183 self._continuation_prompt_html)
1193 1184
1194 1185 self._prompt_started()
1195 1186
1196 1187
1197 1188 class HistoryConsoleWidget(ConsoleWidget):
1198 1189 """ A ConsoleWidget that keeps a history of the commands that have been
1199 1190 executed.
1200 1191 """
1201 1192
1202 1193 #---------------------------------------------------------------------------
1203 1194 # 'object' interface
1204 1195 #---------------------------------------------------------------------------
1205 1196
1206 1197 def __init__(self, *args, **kw):
1207 1198 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1208 1199 self._history = []
1209 1200 self._history_index = 0
1210 1201
1211 1202 #---------------------------------------------------------------------------
1212 1203 # 'ConsoleWidget' public interface
1213 1204 #---------------------------------------------------------------------------
1214 1205
1215 1206 def execute(self, source=None, hidden=False, interactive=False):
1216 1207 """ Reimplemented to the store history.
1217 1208 """
1218 1209 if not hidden:
1219 1210 history = self.input_buffer if source is None else source
1220 1211
1221 1212 executed = super(HistoryConsoleWidget, self).execute(
1222 1213 source, hidden, interactive)
1223 1214
1224 1215 if executed and not hidden:
1225 1216 self._history.append(history.rstrip())
1226 1217 self._history_index = len(self._history)
1227 1218
1228 1219 return executed
1229 1220
1230 1221 #---------------------------------------------------------------------------
1231 1222 # 'ConsoleWidget' abstract interface
1232 1223 #---------------------------------------------------------------------------
1233 1224
1234 1225 def _up_pressed(self):
1235 1226 """ Called when the up key is pressed. Returns whether to continue
1236 1227 processing the event.
1237 1228 """
1238 1229 prompt_cursor = self._get_prompt_cursor()
1239 1230 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1240 1231 self.history_previous()
1241 1232
1242 1233 # Go to the first line of prompt for seemless history scrolling.
1243 1234 cursor = self._get_prompt_cursor()
1244 1235 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1245 1236 self._set_cursor(cursor)
1246 1237
1247 1238 return False
1248 1239 return True
1249 1240
1250 1241 def _down_pressed(self):
1251 1242 """ Called when the down key is pressed. Returns whether to continue
1252 1243 processing the event.
1253 1244 """
1254 1245 end_cursor = self._get_end_cursor()
1255 1246 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1256 1247 self.history_next()
1257 1248 return False
1258 1249 return True
1259 1250
1260 1251 #---------------------------------------------------------------------------
1261 1252 # 'HistoryConsoleWidget' public interface
1262 1253 #---------------------------------------------------------------------------
1263 1254
1264 1255 def history_previous(self):
1265 1256 """ If possible, set the input buffer to the previous item in the
1266 1257 history.
1267 1258 """
1268 1259 if self._history_index > 0:
1269 1260 self._history_index -= 1
1270 1261 self.input_buffer = self._history[self._history_index]
1271 1262
1272 1263 def history_next(self):
1273 1264 """ Set the input buffer to the next item in the history, or a blank
1274 1265 line if there is no subsequent item.
1275 1266 """
1276 1267 if self._history_index < len(self._history):
1277 1268 self._history_index += 1
1278 1269 if self._history_index < len(self._history):
1279 1270 self.input_buffer = self._history[self._history_index]
1280 1271 else:
1281 1272 self.input_buffer = ''
1282 1273
1283 1274 #---------------------------------------------------------------------------
1284 1275 # 'HistoryConsoleWidget' protected interface
1285 1276 #---------------------------------------------------------------------------
1286 1277
1287 1278 def _set_history(self, history):
1288 1279 """ Replace the current history with a sequence of history items.
1289 1280 """
1290 1281 self._history = list(history)
1291 1282 self._history_index = len(self._history)
@@ -1,386 +1,423 b''
1 1 # Standard library imports
2 2 import signal
3 3 import sys
4 4
5 5 # System library imports
6 6 from pygments.lexers import PythonLexer
7 7 from PyQt4 import QtCore, QtGui
8 8 import zmq
9 9
10 10 # Local imports
11 11 from IPython.core.inputsplitter import InputSplitter
12 12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
13 13 from call_tip_widget import CallTipWidget
14 14 from completion_lexer import CompletionLexer
15 15 from console_widget import HistoryConsoleWidget
16 16 from pygments_highlighter import PygmentsHighlighter
17 17
18 18
19 19 class FrontendHighlighter(PygmentsHighlighter):
20 20 """ A PygmentsHighlighter that can be turned on and off and that ignores
21 21 prompts.
22 22 """
23 23
24 24 def __init__(self, frontend):
25 25 super(FrontendHighlighter, self).__init__(frontend._control.document())
26 26 self._current_offset = 0
27 27 self._frontend = frontend
28 28 self.highlighting_on = False
29 29
30 30 def highlightBlock(self, qstring):
31 31 """ Highlight a block of text. Reimplemented to highlight selectively.
32 32 """
33 33 if not self.highlighting_on:
34 34 return
35 35
36 36 # The input to this function is unicode string that may contain
37 37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
38 38 # the string as plain text so we can compare it.
39 39 current_block = self.currentBlock()
40 40 string = self._frontend._get_block_plain_text(current_block)
41 41
42 42 # Decide whether to check for the regular or continuation prompt.
43 43 if current_block.contains(self._frontend._prompt_pos):
44 44 prompt = self._frontend._prompt
45 45 else:
46 46 prompt = self._frontend._continuation_prompt
47 47
48 48 # Don't highlight the part of the string that contains the prompt.
49 49 if string.startswith(prompt):
50 50 self._current_offset = len(prompt)
51 51 qstring.remove(0, len(prompt))
52 52 else:
53 53 self._current_offset = 0
54 54
55 55 PygmentsHighlighter.highlightBlock(self, qstring)
56 56
57 57 def rehighlightBlock(self, block):
58 58 """ Reimplemented to temporarily enable highlighting if disabled.
59 59 """
60 60 old = self.highlighting_on
61 61 self.highlighting_on = True
62 62 super(FrontendHighlighter, self).rehighlightBlock(block)
63 63 self.highlighting_on = old
64 64
65 65 def setFormat(self, start, count, format):
66 66 """ Reimplemented to highlight selectively.
67 67 """
68 68 start += self._current_offset
69 69 PygmentsHighlighter.setFormat(self, start, count, format)
70 70
71 71
72 72 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
73 73 """ A Qt frontend for a generic Python kernel.
74 74 """
75
76 # An option and corresponding signal for overriding the default kernel
77 # interrupt behavior.
78 custom_interrupt = False
79 custom_interrupt_requested = QtCore.pyqtSignal()
80
81 # An option and corresponding signal for overriding the default kernel
82 # restart behavior.
83 custom_restart = False
84 custom_restart_requested = QtCore.pyqtSignal()
75 85
76 86 # Emitted when an 'execute_reply' has been received from the kernel and
77 87 # processed by the FrontendWidget.
78 88 executed = QtCore.pyqtSignal(object)
79
80 # Protected class attributes.
89
90 # Protected class variables.
81 91 _highlighter_class = FrontendHighlighter
82 92 _input_splitter_class = InputSplitter
83 93
84 94 #---------------------------------------------------------------------------
85 95 # 'object' interface
86 96 #---------------------------------------------------------------------------
87 97
88 98 def __init__(self, *args, **kw):
89 99 super(FrontendWidget, self).__init__(*args, **kw)
90 100
91 101 # FrontendWidget protected variables.
92 102 self._call_tip_widget = CallTipWidget(self._control)
93 103 self._completion_lexer = CompletionLexer(PythonLexer())
94 104 self._hidden = False
95 105 self._highlighter = self._highlighter_class(self)
96 106 self._input_splitter = self._input_splitter_class(input_mode='replace')
97 107 self._kernel_manager = None
98 108
99 109 # Configure the ConsoleWidget.
100 110 self.tab_width = 4
101 111 self._set_continuation_prompt('... ')
102 112
103 113 # Connect signal handlers.
104 114 document = self._control.document()
105 115 document.contentsChange.connect(self._document_contents_change)
106 116
107 117 #---------------------------------------------------------------------------
108 118 # 'ConsoleWidget' abstract interface
109 119 #---------------------------------------------------------------------------
110 120
111 121 def _is_complete(self, source, interactive):
112 122 """ Returns whether 'source' can be completely processed and a new
113 123 prompt created. When triggered by an Enter/Return key press,
114 124 'interactive' is True; otherwise, it is False.
115 125 """
116 126 complete = self._input_splitter.push(source.expandtabs(4))
117 127 if interactive:
118 128 complete = not self._input_splitter.push_accepts_more()
119 129 return complete
120 130
121 131 def _execute(self, source, hidden):
122 132 """ Execute 'source'. If 'hidden', do not show any output.
123 133 """
124 134 self.kernel_manager.xreq_channel.execute(source, hidden)
125 135 self._hidden = hidden
126
127 def _execute_interrupt(self):
128 """ Attempts to stop execution. Returns whether this method has an
129 implementation.
130 """
131 self._interrupt_kernel()
132 return True
133 136
134 137 def _prompt_started_hook(self):
135 138 """ Called immediately after a new prompt is displayed.
136 139 """
137 140 if not self._reading:
138 141 self._highlighter.highlighting_on = True
139 142
140 143 def _prompt_finished_hook(self):
141 144 """ Called immediately after a prompt is finished, i.e. when some input
142 145 will be processed and a new prompt displayed.
143 146 """
144 147 if not self._reading:
145 148 self._highlighter.highlighting_on = False
146 149
147 150 def _tab_pressed(self):
148 151 """ Called when the tab key is pressed. Returns whether to continue
149 152 processing the event.
150 153 """
151 154 # Perform tab completion if:
152 155 # 1) The cursor is in the input buffer.
153 156 # 2) There is a non-whitespace character before the cursor.
154 157 text = self._get_input_buffer_cursor_line()
155 158 if text is None:
156 159 return False
157 160 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
158 161 if complete:
159 162 self._complete()
160 163 return not complete
161 164
162 165 #---------------------------------------------------------------------------
163 166 # 'ConsoleWidget' protected interface
164 167 #---------------------------------------------------------------------------
165 168
169 def _event_filter_console_keypress(self, event):
170 """ Reimplemented to allow execution interruption.
171 """
172 key = event.key()
173 if self._executing and self._control_key_down(event.modifiers()):
174 if key == QtCore.Qt.Key_C:
175 self._kernel_interrupt()
176 return True
177 elif key == QtCore.Qt.Key_Period:
178 self._kernel_restart()
179 return True
180 return super(FrontendWidget, self)._event_filter_console_keypress(event)
181
166 182 def _show_continuation_prompt(self):
167 183 """ Reimplemented for auto-indentation.
168 184 """
169 185 super(FrontendWidget, self)._show_continuation_prompt()
170 186 spaces = self._input_splitter.indent_spaces
171 187 self._append_plain_text('\t' * (spaces / self.tab_width))
172 188 self._append_plain_text(' ' * (spaces % self.tab_width))
173 189
174 190 #---------------------------------------------------------------------------
175 191 # 'BaseFrontendMixin' abstract interface
176 192 #---------------------------------------------------------------------------
177 193
178 194 def _handle_complete_reply(self, rep):
179 195 """ Handle replies for tab completion.
180 196 """
181 197 cursor = self._get_cursor()
182 198 if rep['parent_header']['msg_id'] == self._complete_id and \
183 199 cursor.position() == self._complete_pos:
184 200 text = '.'.join(self._get_context())
185 201 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
186 202 self._complete_with_items(cursor, rep['content']['matches'])
187 203
188 204 def _handle_execute_reply(self, msg):
189 205 """ Handles replies for code execution.
190 206 """
191 207 if not self._hidden:
192 208 # Make sure that all output from the SUB channel has been processed
193 209 # before writing a new prompt.
194 210 self.kernel_manager.sub_channel.flush()
195 211
196 212 content = msg['content']
197 213 status = content['status']
198 214 if status == 'ok':
199 215 self._process_execute_ok(msg)
200 216 elif status == 'error':
201 217 self._process_execute_error(msg)
202 218 elif status == 'abort':
203 219 self._process_execute_abort(msg)
204 220
205 221 self._show_interpreter_prompt_for_reply(msg)
206 222 self.executed.emit(msg)
207 223
208 224 def _handle_input_request(self, msg):
209 225 """ Handle requests for raw_input.
210 226 """
211 227 if self._hidden:
212 228 raise RuntimeError('Request for raw input during hidden execution.')
213 229
214 230 # Make sure that all output from the SUB channel has been processed
215 231 # before entering readline mode.
216 232 self.kernel_manager.sub_channel.flush()
217 233
218 234 def callback(line):
219 235 self.kernel_manager.rep_channel.input(line)
220 236 self._readline(msg['content']['prompt'], callback=callback)
221 237
222 238 def _handle_object_info_reply(self, rep):
223 239 """ Handle replies for call tips.
224 240 """
225 241 cursor = self._get_cursor()
226 242 if rep['parent_header']['msg_id'] == self._call_tip_id and \
227 243 cursor.position() == self._call_tip_pos:
228 244 doc = rep['content']['docstring']
229 245 if doc:
230 246 self._call_tip_widget.show_docstring(doc)
231 247
232 248 def _handle_pyout(self, msg):
233 249 """ Handle display hook output.
234 250 """
235 251 if not self._hidden and self._is_from_this_session(msg):
236 252 self._append_plain_text(msg['content']['data'] + '\n')
237 253
238 254 def _handle_stream(self, msg):
239 255 """ Handle stdout, stderr, and stdin.
240 256 """
241 257 if not self._hidden and self._is_from_this_session(msg):
242 258 self._append_plain_text(msg['content']['data'])
243 259 self._control.moveCursor(QtGui.QTextCursor.End)
244 260
245 261 def _started_channels(self):
246 262 """ Called when the KernelManager channels have started listening or
247 263 when the frontend is assigned an already listening KernelManager.
248 264 """
249 265 self._control.clear()
250 266 self._append_plain_text(self._get_banner())
251 267 self._show_interpreter_prompt()
252 268
253 269 def _stopped_channels(self):
254 270 """ Called when the KernelManager channels have stopped listening or
255 271 when a listening KernelManager is removed from the frontend.
256 272 """
257 273 self._executing = self._reading = False
258 274 self._highlighter.highlighting_on = False
259 275
260 276 #---------------------------------------------------------------------------
261 277 # 'FrontendWidget' interface
262 278 #---------------------------------------------------------------------------
263 279
264 280 def execute_file(self, path, hidden=False):
265 281 """ Attempts to execute file with 'path'. If 'hidden', no output is
266 282 shown.
267 283 """
268 284 self.execute('execfile("%s")' % path, hidden=hidden)
269 285
270 286 #---------------------------------------------------------------------------
271 287 # 'FrontendWidget' protected interface
272 288 #---------------------------------------------------------------------------
273 289
274 290 def _call_tip(self):
275 291 """ Shows a call tip, if appropriate, at the current cursor location.
276 292 """
277 293 # Decide if it makes sense to show a call tip
278 294 cursor = self._get_cursor()
279 295 cursor.movePosition(QtGui.QTextCursor.Left)
280 296 document = self._control.document()
281 297 if document.characterAt(cursor.position()).toAscii() != '(':
282 298 return False
283 299 context = self._get_context(cursor)
284 300 if not context:
285 301 return False
286 302
287 303 # Send the metadata request to the kernel
288 304 name = '.'.join(context)
289 305 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
290 306 self._call_tip_pos = self._get_cursor().position()
291 307 return True
292 308
293 309 def _complete(self):
294 310 """ Performs completion at the current cursor location.
295 311 """
296 312 # Decide if it makes sense to do completion
297 313 context = self._get_context()
298 314 if not context:
299 315 return False
300 316
301 317 # Send the completion request to the kernel
302 318 self._complete_id = self.kernel_manager.xreq_channel.complete(
303 319 '.'.join(context), # text
304 320 self._get_input_buffer_cursor_line(), # line
305 321 self._get_input_buffer_cursor_column(), # cursor_pos
306 322 self.input_buffer) # block
307 323 self._complete_pos = self._get_cursor().position()
308 324 return True
309 325
310 326 def _get_banner(self):
311 327 """ Gets a banner to display at the beginning of a session.
312 328 """
313 329 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
314 330 '"license" for more information.'
315 331 return banner % (sys.version, sys.platform)
316 332
317 333 def _get_context(self, cursor=None):
318 334 """ Gets the context at the current cursor location.
319 335 """
320 336 if cursor is None:
321 337 cursor = self._get_cursor()
322 338 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
323 339 QtGui.QTextCursor.KeepAnchor)
324 340 text = str(cursor.selection().toPlainText())
325 341 return self._completion_lexer.get_context(text)
326 342
327 def _interrupt_kernel(self):
328 """ Attempts to the interrupt the kernel.
343 def _kernel_interrupt(self):
344 """ Attempts to interrupt the running kernel.
329 345 """
330 if self.kernel_manager.has_kernel:
346 if self.custom_interrupt:
347 self.custom_interrupt_requested.emit()
348 elif self.kernel_manager.has_kernel:
331 349 self.kernel_manager.signal_kernel(signal.SIGINT)
332 350 else:
333 351 self._append_plain_text('Kernel process is either remote or '
334 352 'unspecified. Cannot interrupt.\n')
335 353
354 def _kernel_restart(self):
355 """ Attempts to restart the running kernel.
356 """
357 if self.custom_restart:
358 self.custom_restart_requested.emit()
359 elif self.kernel_manager.has_kernel:
360 try:
361 self.kernel_manager.restart_kernel()
362 except RuntimeError:
363 message = 'Kernel started externally. Cannot restart.\n'
364 self._append_plain_text(message)
365 else:
366 self._stopped_channels()
367 self._append_plain_text('Kernel restarting...\n')
368 self._show_interpreter_prompt()
369 else:
370 self._append_plain_text('Kernel process is either remote or '
371 'unspecified. Cannot restart.\n')
372
336 373 def _process_execute_abort(self, msg):
337 374 """ Process a reply for an aborted execution request.
338 375 """
339 376 self._append_plain_text("ERROR: execution aborted\n")
340 377
341 378 def _process_execute_error(self, msg):
342 379 """ Process a reply for an execution request that resulted in an error.
343 380 """
344 381 content = msg['content']
345 382 traceback = ''.join(content['traceback'])
346 383 self._append_plain_text(traceback)
347 384
348 385 def _process_execute_ok(self, msg):
349 386 """ Process a reply for a successful execution equest.
350 387 """
351 388 payload = msg['content']['payload']
352 389 for item in payload:
353 390 if not self._process_execute_payload(item):
354 391 warning = 'Received unknown payload of type %s\n'
355 392 self._append_plain_text(warning % repr(item['source']))
356 393
357 394 def _process_execute_payload(self, item):
358 395 """ Process a single payload item from the list of payload items in an
359 396 execution reply. Returns whether the payload was handled.
360 397 """
361 398 # The basic FrontendWidget doesn't handle payloads, as they are a
362 399 # mechanism for going beyond the standard Python interpreter model.
363 400 return False
364 401
365 402 def _show_interpreter_prompt(self):
366 403 """ Shows a prompt for the interpreter.
367 404 """
368 405 self._show_prompt('>>> ')
369 406
370 407 def _show_interpreter_prompt_for_reply(self, msg):
371 408 """ Shows a prompt for the interpreter given an 'execute_reply' message.
372 409 """
373 410 self._show_interpreter_prompt()
374 411
375 412 #------ Signal handlers ----------------------------------------------------
376 413
377 414 def _document_contents_change(self, position, removed, added):
378 415 """ Called whenever the document's content changes. Display a call tip
379 416 if appropriate.
380 417 """
381 418 # Calculate where the cursor should be *after* the change:
382 419 position += added
383 420
384 421 document = self._control.document()
385 422 if position == self._get_cursor().position():
386 423 self._call_tip()
@@ -1,126 +1,124 b''
1 import os
2
3 1 # System library imports
4 2 from PyQt4 import QtCore, QtGui
5 3
6 4 # Local imports
7 5 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
8 6 from ipython_widget import IPythonWidget
9 7
10 8
11 9 class RichIPythonWidget(IPythonWidget):
12 10 """ An IPythonWidget that supports rich text, including lists, images, and
13 11 tables. Note that raw performance will be reduced compared to the plain
14 12 text version.
15 13 """
16 14
17 15 # RichIPythonWidget protected class variables.
18 16 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
19 17 _svg_text_format_property = 1
20 18
21 19 #---------------------------------------------------------------------------
22 20 # 'object' interface
23 21 #---------------------------------------------------------------------------
24 22
25 23 def __init__(self, *args, **kw):
26 24 """ Create a RichIPythonWidget.
27 25 """
28 26 kw['kind'] = 'rich'
29 27 super(RichIPythonWidget, self).__init__(*args, **kw)
30 28
31 29 #---------------------------------------------------------------------------
32 30 # 'ConsoleWidget' protected interface
33 31 #---------------------------------------------------------------------------
34 32
35 33 def _show_context_menu(self, pos):
36 34 """ Reimplemented to show a custom context menu for images.
37 35 """
38 36 format = self._control.cursorForPosition(pos).charFormat()
39 37 name = format.stringProperty(QtGui.QTextFormat.ImageName)
40 38 if name.isEmpty():
41 39 super(RichIPythonWidget, self)._show_context_menu(pos)
42 40 else:
43 41 menu = QtGui.QMenu()
44 42
45 43 menu.addAction('Copy Image', lambda: self._copy_image(name))
46 44 menu.addAction('Save Image As...', lambda: self._save_image(name))
47 45 menu.addSeparator()
48 46
49 47 svg = format.stringProperty(self._svg_text_format_property)
50 48 if not svg.isEmpty():
51 49 menu.addSeparator()
52 50 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
53 51 menu.addAction('Save SVG As...',
54 52 lambda: save_svg(svg, self._control))
55 53
56 54 menu.exec_(self._control.mapToGlobal(pos))
57 55
58 56 #---------------------------------------------------------------------------
59 57 # 'FrontendWidget' protected interface
60 58 #---------------------------------------------------------------------------
61 59
62 60 def _process_execute_payload(self, item):
63 61 """ Reimplemented to handle matplotlib plot payloads.
64 62 """
65 63 if item['source'] == self._payload_source_plot:
66 64 if item['format'] == 'svg':
67 65 svg = item['data']
68 66 try:
69 67 image = svg_to_image(svg)
70 68 except ValueError:
71 69 self._append_plain_text('Received invalid plot data.')
72 70 else:
73 71 format = self._add_image(image)
74 72 format.setProperty(self._svg_text_format_property, svg)
75 73 cursor = self._get_end_cursor()
76 74 cursor.insertBlock()
77 75 cursor.insertImage(format)
78 76 cursor.insertBlock()
79 77 return True
80 78 else:
81 79 # Add other plot formats here!
82 80 return False
83 81 else:
84 82 return super(RichIPythonWidget, self)._process_execute_payload(item)
85 83
86 84 #---------------------------------------------------------------------------
87 85 # 'RichIPythonWidget' protected interface
88 86 #---------------------------------------------------------------------------
89 87
90 88 def _add_image(self, image):
91 89 """ Adds the specified QImage to the document and returns a
92 90 QTextImageFormat that references it.
93 91 """
94 92 document = self._control.document()
95 93 name = QtCore.QString.number(image.cacheKey())
96 94 document.addResource(QtGui.QTextDocument.ImageResource,
97 95 QtCore.QUrl(name), image)
98 96 format = QtGui.QTextImageFormat()
99 97 format.setName(name)
100 98 return format
101 99
102 100 def _copy_image(self, name):
103 101 """ Copies the ImageResource with 'name' to the clipboard.
104 102 """
105 103 image = self._get_image(name)
106 104 QtGui.QApplication.clipboard().setImage(image)
107 105
108 106 def _get_image(self, name):
109 107 """ Returns the QImage stored as the ImageResource with 'name'.
110 108 """
111 109 document = self._control.document()
112 110 variant = document.resource(QtGui.QTextDocument.ImageResource,
113 111 QtCore.QUrl(name))
114 112 return variant.toPyObject()
115 113
116 114 def _save_image(self, name, format='PNG'):
117 115 """ Shows a save dialog for the ImageResource with 'name'.
118 116 """
119 117 dialog = QtGui.QFileDialog(self._control, 'Save Image')
120 118 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
121 119 dialog.setDefaultSuffix(format.lower())
122 120 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
123 121 if dialog.exec_():
124 122 filename = dialog.selectedFiles()[0]
125 123 image = self._get_image(name)
126 124 image.save(filename, format)
@@ -1,617 +1,632 b''
1 1 """Base classes to manage the interaction with a running kernel.
2 2
3 3 Todo
4 4 ====
5 5
6 6 * Create logger to handle debugging and console messages.
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2008-2010 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # Standard library imports.
21 21 from Queue import Queue, Empty
22 22 from subprocess import Popen
23 23 from threading import Thread
24 24 import time
25 25
26 26 # System library imports.
27 27 import zmq
28 28 from zmq import POLLIN, POLLOUT, POLLERR
29 29 from zmq.eventloop import ioloop
30 30
31 31 # Local imports.
32 32 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
33 33 from session import Session
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Constants and exceptions
37 37 #-----------------------------------------------------------------------------
38 38
39 39 LOCALHOST = '127.0.0.1'
40 40
41 41 class InvalidPortNumber(Exception):
42 42 pass
43 43
44 44 #-----------------------------------------------------------------------------
45 45 # ZMQ Socket Channel classes
46 46 #-----------------------------------------------------------------------------
47 47
48 48 class ZmqSocketChannel(Thread):
49 49 """The base class for the channels that use ZMQ sockets.
50 50 """
51 51 context = None
52 52 session = None
53 53 socket = None
54 54 ioloop = None
55 55 iostate = None
56 56 _address = None
57 57
58 58 def __init__(self, context, session, address):
59 59 """Create a channel
60 60
61 61 Parameters
62 62 ----------
63 63 context : :class:`zmq.Context`
64 64 The ZMQ context to use.
65 65 session : :class:`session.Session`
66 66 The session to use.
67 67 address : tuple
68 68 Standard (ip, port) tuple that the kernel is listening on.
69 69 """
70 70 super(ZmqSocketChannel, self).__init__()
71 71 self.daemon = True
72 72
73 73 self.context = context
74 74 self.session = session
75 75 if address[1] == 0:
76 76 message = 'The port number for a channel cannot be 0.'
77 77 raise InvalidPortNumber(message)
78 78 self._address = address
79 79
80 80 def stop(self):
81 81 """Stop the channel's activity.
82 82
83 83 This calls :method:`Thread.join` and returns when the thread
84 84 terminates. :class:`RuntimeError` will be raised if
85 85 :method:`self.start` is called again.
86 86 """
87 87 self.join()
88 88
89 89 @property
90 90 def address(self):
91 91 """Get the channel's address as an (ip, port) tuple.
92 92
93 93 By the default, the address is (localhost, 0), where 0 means a random
94 94 port.
95 95 """
96 96 return self._address
97 97
98 98 def add_io_state(self, state):
99 99 """Add IO state to the eventloop.
100 100
101 101 Parameters
102 102 ----------
103 103 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
104 104 The IO state flag to set.
105 105
106 106 This is thread safe as it uses the thread safe IOLoop.add_callback.
107 107 """
108 108 def add_io_state_callback():
109 109 if not self.iostate & state:
110 110 self.iostate = self.iostate | state
111 111 self.ioloop.update_handler(self.socket, self.iostate)
112 112 self.ioloop.add_callback(add_io_state_callback)
113 113
114 114 def drop_io_state(self, state):
115 115 """Drop IO state from the eventloop.
116 116
117 117 Parameters
118 118 ----------
119 119 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
120 120 The IO state flag to set.
121 121
122 122 This is thread safe as it uses the thread safe IOLoop.add_callback.
123 123 """
124 124 def drop_io_state_callback():
125 125 if self.iostate & state:
126 126 self.iostate = self.iostate & (~state)
127 127 self.ioloop.update_handler(self.socket, self.iostate)
128 128 self.ioloop.add_callback(drop_io_state_callback)
129 129
130 130
131 131 class XReqSocketChannel(ZmqSocketChannel):
132 132 """The XREQ channel for issues request/replies to the kernel.
133 133 """
134 134
135 135 command_queue = None
136 136
137 137 def __init__(self, context, session, address):
138 138 self.command_queue = Queue()
139 139 super(XReqSocketChannel, self).__init__(context, session, address)
140 140
141 141 def run(self):
142 142 """The thread's main activity. Call start() instead."""
143 143 self.socket = self.context.socket(zmq.XREQ)
144 144 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
145 145 self.socket.connect('tcp://%s:%i' % self.address)
146 146 self.ioloop = ioloop.IOLoop()
147 147 self.iostate = POLLERR|POLLIN
148 148 self.ioloop.add_handler(self.socket, self._handle_events,
149 149 self.iostate)
150 150 self.ioloop.start()
151 151
152 152 def stop(self):
153 153 self.ioloop.stop()
154 154 super(XReqSocketChannel, self).stop()
155 155
156 156 def call_handlers(self, msg):
157 157 """This method is called in the ioloop thread when a message arrives.
158 158
159 159 Subclasses should override this method to handle incoming messages.
160 160 It is important to remember that this method is called in the thread
161 161 so that some logic must be done to ensure that the application leve
162 162 handlers are called in the application thread.
163 163 """
164 164 raise NotImplementedError('call_handlers must be defined in a subclass.')
165 165
166 166 def execute(self, code, silent=False):
167 167 """Execute code in the kernel.
168 168
169 169 Parameters
170 170 ----------
171 171 code : str
172 172 A string of Python code.
173 173 silent : bool, optional (default False)
174 174 If set, the kernel will execute the code as quietly possible.
175 175
176 176 Returns
177 177 -------
178 178 The msg_id of the message sent.
179 179 """
180 180 # Create class for content/msg creation. Related to, but possibly
181 181 # not in Session.
182 182 content = dict(code=code, silent=silent)
183 183 msg = self.session.msg('execute_request', content)
184 184 self._queue_request(msg)
185 185 return msg['header']['msg_id']
186 186
187 187 def complete(self, text, line, cursor_pos, block=None):
188 188 """Tab complete text in the kernel's namespace.
189 189
190 190 Parameters
191 191 ----------
192 192 text : str
193 193 The text to complete.
194 194 line : str
195 195 The full line of text that is the surrounding context for the
196 196 text to complete.
197 197 cursor_pos : int
198 198 The position of the cursor in the line where the completion was
199 199 requested.
200 200 block : str, optional
201 201 The full block of code in which the completion is being requested.
202 202
203 203 Returns
204 204 -------
205 205 The msg_id of the message sent.
206 206 """
207 207 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
208 208 msg = self.session.msg('complete_request', content)
209 209 self._queue_request(msg)
210 210 return msg['header']['msg_id']
211 211
212 212 def object_info(self, oname):
213 213 """Get metadata information about an object.
214 214
215 215 Parameters
216 216 ----------
217 217 oname : str
218 218 A string specifying the object name.
219 219
220 220 Returns
221 221 -------
222 222 The msg_id of the message sent.
223 223 """
224 224 content = dict(oname=oname)
225 225 msg = self.session.msg('object_info_request', content)
226 226 self._queue_request(msg)
227 227 return msg['header']['msg_id']
228 228
229 229 def history(self, index=None, raw=False, output=True):
230 230 """Get the history list.
231 231
232 232 Parameters
233 233 ----------
234 234 index : n or (n1, n2) or None
235 235 If n, then the last entries. If a tuple, then all in
236 236 range(n1, n2). If None, then all entries. Raises IndexError if
237 237 the format of index is incorrect.
238 238 raw : bool
239 239 If True, return the raw input.
240 240 output : bool
241 241 If True, then return the output as well.
242 242
243 243 Returns
244 244 -------
245 245 The msg_id of the message sent.
246 246 """
247 247 content = dict(index=index, raw=raw, output=output)
248 248 msg = self.session.msg('history_request', content)
249 249 self._queue_request(msg)
250 250 return msg['header']['msg_id']
251 251
252 252 def prompt(self):
253 253 """Requests a prompt number from the kernel.
254 254
255 255 Returns
256 256 -------
257 257 The msg_id of the message sent.
258 258 """
259 259 msg = self.session.msg('prompt_request')
260 260 self._queue_request(msg)
261 261 return msg['header']['msg_id']
262 262
263 263 def _handle_events(self, socket, events):
264 264 if events & POLLERR:
265 265 self._handle_err()
266 266 if events & POLLOUT:
267 267 self._handle_send()
268 268 if events & POLLIN:
269 269 self._handle_recv()
270 270
271 271 def _handle_recv(self):
272 272 msg = self.socket.recv_json()
273 273 self.call_handlers(msg)
274 274
275 275 def _handle_send(self):
276 276 try:
277 277 msg = self.command_queue.get(False)
278 278 except Empty:
279 279 pass
280 280 else:
281 281 self.socket.send_json(msg)
282 282 if self.command_queue.empty():
283 283 self.drop_io_state(POLLOUT)
284 284
285 285 def _handle_err(self):
286 286 # We don't want to let this go silently, so eventually we should log.
287 287 raise zmq.ZMQError()
288 288
289 289 def _queue_request(self, msg):
290 290 self.command_queue.put(msg)
291 291 self.add_io_state(POLLOUT)
292 292
293 293
294 294 class SubSocketChannel(ZmqSocketChannel):
295 295 """The SUB channel which listens for messages that the kernel publishes.
296 296 """
297 297
298 298 def __init__(self, context, session, address):
299 299 super(SubSocketChannel, self).__init__(context, session, address)
300 300
301 301 def run(self):
302 302 """The thread's main activity. Call start() instead."""
303 303 self.socket = self.context.socket(zmq.SUB)
304 304 self.socket.setsockopt(zmq.SUBSCRIBE,'')
305 305 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
306 306 self.socket.connect('tcp://%s:%i' % self.address)
307 307 self.ioloop = ioloop.IOLoop()
308 308 self.iostate = POLLIN|POLLERR
309 309 self.ioloop.add_handler(self.socket, self._handle_events,
310 310 self.iostate)
311 311 self.ioloop.start()
312 312
313 313 def stop(self):
314 314 self.ioloop.stop()
315 315 super(SubSocketChannel, self).stop()
316 316
317 317 def call_handlers(self, msg):
318 318 """This method is called in the ioloop thread when a message arrives.
319 319
320 320 Subclasses should override this method to handle incoming messages.
321 321 It is important to remember that this method is called in the thread
322 322 so that some logic must be done to ensure that the application leve
323 323 handlers are called in the application thread.
324 324 """
325 325 raise NotImplementedError('call_handlers must be defined in a subclass.')
326 326
327 327 def flush(self, timeout=1.0):
328 328 """Immediately processes all pending messages on the SUB channel.
329 329
330 330 Callers should use this method to ensure that :method:`call_handlers`
331 331 has been called for all messages that have been received on the
332 332 0MQ SUB socket of this channel.
333 333
334 334 This method is thread safe.
335 335
336 336 Parameters
337 337 ----------
338 338 timeout : float, optional
339 339 The maximum amount of time to spend flushing, in seconds. The
340 340 default is one second.
341 341 """
342 342 # We do the IOLoop callback process twice to ensure that the IOLoop
343 343 # gets to perform at least one full poll.
344 344 stop_time = time.time() + timeout
345 345 for i in xrange(2):
346 346 self._flushed = False
347 347 self.ioloop.add_callback(self._flush)
348 348 while not self._flushed and time.time() < stop_time:
349 349 time.sleep(0.01)
350 350
351 351 def _handle_events(self, socket, events):
352 352 # Turn on and off POLLOUT depending on if we have made a request
353 353 if events & POLLERR:
354 354 self._handle_err()
355 355 if events & POLLIN:
356 356 self._handle_recv()
357 357
358 358 def _handle_err(self):
359 359 # We don't want to let this go silently, so eventually we should log.
360 360 raise zmq.ZMQError()
361 361
362 362 def _handle_recv(self):
363 363 # Get all of the messages we can
364 364 while True:
365 365 try:
366 366 msg = self.socket.recv_json(zmq.NOBLOCK)
367 367 except zmq.ZMQError:
368 368 # Check the errno?
369 369 # Will this trigger POLLERR?
370 370 break
371 371 else:
372 372 self.call_handlers(msg)
373 373
374 374 def _flush(self):
375 375 """Callback for :method:`self.flush`."""
376 376 self._flushed = True
377 377
378 378
379 379 class RepSocketChannel(ZmqSocketChannel):
380 380 """A reply channel to handle raw_input requests that the kernel makes."""
381 381
382 382 msg_queue = None
383 383
384 384 def __init__(self, context, session, address):
385 385 self.msg_queue = Queue()
386 386 super(RepSocketChannel, self).__init__(context, session, address)
387 387
388 388 def run(self):
389 389 """The thread's main activity. Call start() instead."""
390 390 self.socket = self.context.socket(zmq.XREQ)
391 391 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
392 392 self.socket.connect('tcp://%s:%i' % self.address)
393 393 self.ioloop = ioloop.IOLoop()
394 394 self.iostate = POLLERR|POLLIN
395 395 self.ioloop.add_handler(self.socket, self._handle_events,
396 396 self.iostate)
397 397 self.ioloop.start()
398 398
399 399 def stop(self):
400 400 self.ioloop.stop()
401 401 super(RepSocketChannel, self).stop()
402 402
403 403 def call_handlers(self, msg):
404 404 """This method is called in the ioloop thread when a message arrives.
405 405
406 406 Subclasses should override this method to handle incoming messages.
407 407 It is important to remember that this method is called in the thread
408 408 so that some logic must be done to ensure that the application leve
409 409 handlers are called in the application thread.
410 410 """
411 411 raise NotImplementedError('call_handlers must be defined in a subclass.')
412 412
413 413 def input(self, string):
414 414 """Send a string of raw input to the kernel."""
415 415 content = dict(value=string)
416 416 msg = self.session.msg('input_reply', content)
417 417 self._queue_reply(msg)
418 418
419 419 def _handle_events(self, socket, events):
420 420 if events & POLLERR:
421 421 self._handle_err()
422 422 if events & POLLOUT:
423 423 self._handle_send()
424 424 if events & POLLIN:
425 425 self._handle_recv()
426 426
427 427 def _handle_recv(self):
428 428 msg = self.socket.recv_json()
429 429 self.call_handlers(msg)
430 430
431 431 def _handle_send(self):
432 432 try:
433 433 msg = self.msg_queue.get(False)
434 434 except Empty:
435 435 pass
436 436 else:
437 437 self.socket.send_json(msg)
438 438 if self.msg_queue.empty():
439 439 self.drop_io_state(POLLOUT)
440 440
441 441 def _handle_err(self):
442 442 # We don't want to let this go silently, so eventually we should log.
443 443 raise zmq.ZMQError()
444 444
445 445 def _queue_reply(self, msg):
446 446 self.msg_queue.put(msg)
447 447 self.add_io_state(POLLOUT)
448 448
449 449
450 450 #-----------------------------------------------------------------------------
451 451 # Main kernel manager class
452 452 #-----------------------------------------------------------------------------
453 453
454 454 class KernelManager(HasTraits):
455 455 """ Manages a kernel for a frontend.
456 456
457 457 The SUB channel is for the frontend to receive messages published by the
458 458 kernel.
459 459
460 460 The REQ channel is for the frontend to make requests of the kernel.
461 461
462 462 The REP channel is for the kernel to request stdin (raw_input) from the
463 463 frontend.
464 464 """
465 465 # The PyZMQ Context to use for communication with the kernel.
466 466 context = Instance(zmq.Context,(),{})
467 467
468 468 # The Session to use for communication with the kernel.
469 469 session = Instance(Session,(),{})
470 470
471 471 # The kernel process with which the KernelManager is communicating.
472 472 kernel = Instance(Popen)
473 473
474 474 # The addresses for the communication channels.
475 475 xreq_address = TCPAddress((LOCALHOST, 0))
476 476 sub_address = TCPAddress((LOCALHOST, 0))
477 477 rep_address = TCPAddress((LOCALHOST, 0))
478 478
479 479 # The classes to use for the various channels.
480 480 xreq_channel_class = Type(XReqSocketChannel)
481 481 sub_channel_class = Type(SubSocketChannel)
482 482 rep_channel_class = Type(RepSocketChannel)
483 483
484 484 # Protected traits.
485 _launch_args = Any
485 486 _xreq_channel = Any
486 487 _sub_channel = Any
487 488 _rep_channel = Any
488 489
489 490 #--------------------------------------------------------------------------
490 491 # Channel management methods:
491 492 #--------------------------------------------------------------------------
492 493
493 494 def start_channels(self):
494 495 """Starts the channels for this kernel.
495 496
496 497 This will create the channels if they do not exist and then start
497 498 them. If port numbers of 0 are being used (random ports) then you
498 499 must first call :method:`start_kernel`. If the channels have been
499 500 stopped and you call this, :class:`RuntimeError` will be raised.
500 501 """
501 502 self.xreq_channel.start()
502 503 self.sub_channel.start()
503 504 self.rep_channel.start()
504 505
505 506 def stop_channels(self):
506 507 """Stops the channels for this kernel.
507 508
508 509 This stops the channels by joining their threads. If the channels
509 510 were not started, :class:`RuntimeError` will be raised.
510 511 """
511 512 self.xreq_channel.stop()
512 513 self.sub_channel.stop()
513 514 self.rep_channel.stop()
514 515
515 516 @property
516 517 def channels_running(self):
517 518 """Are all of the channels created and running?"""
518 519 return self.xreq_channel.is_alive() \
519 520 and self.sub_channel.is_alive() \
520 521 and self.rep_channel.is_alive()
521 522
522 523 #--------------------------------------------------------------------------
523 524 # Kernel process management methods:
524 525 #--------------------------------------------------------------------------
525 526
526 def start_kernel(self, ipython=True, **kw):
527 def start_kernel(self, **kw):
527 528 """Starts a kernel process and configures the manager to use it.
528 529
529 530 If random ports (port=0) are being used, this method must be called
530 531 before the channels are created.
531 532
532 533 Parameters:
533 534 -----------
534 535 ipython : bool, optional (default True)
535 536 Whether to use an IPython kernel instead of a plain Python kernel.
536 537 """
537 538 xreq, sub, rep = self.xreq_address, self.sub_address, self.rep_address
538 539 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or rep[0] != LOCALHOST:
539 540 raise RuntimeError("Can only launch a kernel on localhost."
540 541 "Make sure that the '*_address' attributes are "
541 542 "configured properly.")
542 543
543 if ipython:
544 self._launch_args = kw.copy()
545 if kw.pop('ipython', True):
544 546 from ipkernel import launch_kernel as launch
545 547 else:
546 548 from pykernel import launch_kernel as launch
547 549 self.kernel, xrep, pub, req = launch(xrep_port=xreq[1], pub_port=sub[1],
548 550 req_port=rep[1], **kw)
549 551 self.xreq_address = (LOCALHOST, xrep)
550 552 self.sub_address = (LOCALHOST, pub)
551 553 self.rep_address = (LOCALHOST, req)
552 554
555 def restart_kernel(self):
556 """Restarts a kernel with the same arguments that were used to launch
557 it. If the old kernel was launched with random ports, the same ports
558 will be used for the new kernel.
559 """
560 if self._launch_args is None:
561 raise RuntimeError("Cannot restart the kernel. "
562 "No previous call to 'start_kernel'.")
563 else:
564 if self.has_kernel:
565 self.kill_kernel()
566 self.start_kernel(*self._launch_args)
567
553 568 @property
554 569 def has_kernel(self):
555 570 """Returns whether a kernel process has been specified for the kernel
556 571 manager.
557 572 """
558 573 return self.kernel is not None
559 574
560 575 def kill_kernel(self):
561 576 """ Kill the running kernel. """
562 577 if self.kernel is not None:
563 578 self.kernel.kill()
564 579 self.kernel = None
565 580 else:
566 581 raise RuntimeError("Cannot kill kernel. No kernel is running!")
567 582
568 583 def signal_kernel(self, signum):
569 584 """ Sends a signal to the kernel. """
570 585 if self.kernel is not None:
571 586 self.kernel.send_signal(signum)
572 587 else:
573 588 raise RuntimeError("Cannot signal kernel. No kernel is running!")
574 589
575 590 @property
576 591 def is_alive(self):
577 592 """Is the kernel process still running?"""
578 593 if self.kernel is not None:
579 594 if self.kernel.poll() is None:
580 595 return True
581 596 else:
582 597 return False
583 598 else:
584 599 # We didn't start the kernel with this KernelManager so we don't
585 600 # know if it is running. We should use a heartbeat for this case.
586 601 return True
587 602
588 603 #--------------------------------------------------------------------------
589 604 # Channels used for communication with the kernel:
590 605 #--------------------------------------------------------------------------
591 606
592 607 @property
593 608 def xreq_channel(self):
594 609 """Get the REQ socket channel object to make requests of the kernel."""
595 610 if self._xreq_channel is None:
596 611 self._xreq_channel = self.xreq_channel_class(self.context,
597 612 self.session,
598 613 self.xreq_address)
599 614 return self._xreq_channel
600 615
601 616 @property
602 617 def sub_channel(self):
603 618 """Get the SUB socket channel object."""
604 619 if self._sub_channel is None:
605 620 self._sub_channel = self.sub_channel_class(self.context,
606 621 self.session,
607 622 self.sub_address)
608 623 return self._sub_channel
609 624
610 625 @property
611 626 def rep_channel(self):
612 627 """Get the REP socket channel object to handle stdin (raw_input)."""
613 628 if self._rep_channel is None:
614 629 self._rep_channel = self.rep_channel_class(self.context,
615 630 self.session,
616 631 self.rep_address)
617 632 return self._rep_channel
General Comments 0
You need to be logged in to leave comments. Login now