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