##// END OF EJS Templates
Merge branch 'epatters-qtfrontend' into kernelmanager...
Brian Granger -
r2726:7ecaeab5 merge
parent child Browse files
Show More
@@ -0,0 +1,131 b''
1 # Standard library imports
2 import re
3
4 # System library imports
5 from PyQt4 import QtCore, QtGui
6
7
8 class AnsiCodeProcessor(object):
9 """ Translates ANSI escape codes into readable attributes.
10 """
11
12 # Protected class variables.
13 _ansi_commands = 'ABCDEFGHJKSTfmnsu'
14 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands)
15
16 def __init__(self):
17 self.reset()
18
19 def reset(self):
20 """ Reset attributs to their default values.
21 """
22 self.intensity = 0
23 self.italic = False
24 self.bold = False
25 self.underline = False
26 self.foreground_color = None
27 self.background_color = None
28
29 def split_string(self, string):
30 """ Yields substrings for which the same escape code applies.
31 """
32 start = 0
33
34 for match in self._ansi_pattern.finditer(string):
35 substring = string[start:match.start()]
36 if substring:
37 yield substring
38 start = match.end()
39
40 params = map(int, match.group(1).split(';'))
41 self.set_csi_code(match.group(2), params)
42
43 substring = string[start:]
44 if substring:
45 yield substring
46
47 def set_csi_code(self, command, params=[]):
48 """ Set attributes based on CSI (Control Sequence Introducer) code.
49
50 Parameters
51 ----------
52 command : str
53 The code identifier, i.e. the final character in the sequence.
54
55 params : sequence of integers, optional
56 The parameter codes for the command.
57 """
58 if command == 'm': # SGR - Select Graphic Rendition
59 for code in params:
60 self.set_sgr_code(code)
61
62 def set_sgr_code(self, code):
63 """ Set attributes based on SGR (Select Graphic Rendition) code.
64 """
65 if code == 0:
66 self.reset()
67 elif code == 1:
68 self.intensity = 1
69 self.bold = True
70 elif code == 2:
71 self.intensity = 0
72 elif code == 3:
73 self.italic = True
74 elif code == 4:
75 self.underline = True
76 elif code == 22:
77 self.intensity = 0
78 self.bold = False
79 elif code == 23:
80 self.italic = False
81 elif code == 24:
82 self.underline = False
83 elif code >= 30 and code <= 37:
84 self.foreground_color = code - 30
85 elif code == 39:
86 self.foreground_color = None
87 elif code >= 40 and code <= 47:
88 self.background_color = code - 40
89 elif code == 49:
90 self.background_color = None
91
92
93 class QtAnsiCodeProcessor(AnsiCodeProcessor):
94 """ Translates ANSI escape codes into QTextCharFormats.
95 """
96
97 # A map from color codes to RGB colors.
98 ansi_colors = ( # Normal, Bright/Light
99 ('#000000', '#7f7f7f'), # 0: black
100 ('#cd0000', '#ff0000'), # 1: red
101 ('#00cd00', '#00ff00'), # 2: green
102 ('#cdcd00', '#ffff00'), # 3: yellow
103 ('#0000ee', '#0000ff'), # 4: blue
104 ('#cd00cd', '#ff00ff'), # 5: magenta
105 ('#00cdcd', '#00ffff'), # 6: cyan
106 ('#e5e5e5', '#ffffff')) # 7: white
107
108 def get_format(self):
109 """ Returns a QTextCharFormat that encodes the current style attributes.
110 """
111 format = QtGui.QTextCharFormat()
112
113 # Set foreground color
114 if self.foreground_color is not None:
115 color = self.ansi_colors[self.foreground_color][self.intensity]
116 format.setForeground(QtGui.QColor(color))
117
118 # Set background color
119 if self.background_color is not None:
120 color = self.ansi_colors[self.background_color][self.intensity]
121 format.setBackground(QtGui.QColor(color))
122
123 # Set font weight/style options
124 if self.bold:
125 format.setFontWeight(QtGui.QFont.Bold)
126 else:
127 format.setFontWeight(QtGui.QFont.Normal)
128 format.setFontItalic(self.italic)
129 format.setFontUnderline(self.underline)
130
131 return format
@@ -0,0 +1,32 b''
1 # Standard library imports
2 import unittest
3
4 # Local imports
5 from IPython.frontend.qt.console.ansi_code_processor import AnsiCodeProcessor
6
7
8 class TestAnsiCodeProcessor(unittest.TestCase):
9
10 def setUp(self):
11 self.processor = AnsiCodeProcessor()
12
13 def testColors(self):
14 string = "first\x1b[34mblue\x1b[0mlast"
15 i = -1
16 for i, substring in enumerate(self.processor.split_string(string)):
17 if i == 0:
18 self.assertEquals(substring, 'first')
19 self.assertEquals(self.processor.foreground_color, None)
20 elif i == 1:
21 self.assertEquals(substring, 'blue')
22 self.assertEquals(self.processor.foreground_color, 4)
23 elif i == 2:
24 self.assertEquals(substring, 'last')
25 self.assertEquals(self.processor.foreground_color, None)
26 else:
27 self.fail("Too many substrings.")
28 self.assertEquals(i, 2, "Too few substrings.")
29
30
31 if __name__ == '__main__':
32 unittest.main()
@@ -31,7 +31,7 b' class CompletionLexer(object):'
31 31 not string.endswith('\n'):
32 32 reversed_tokens.pop(0)
33 33
34 current_op = unicode()
34 current_op = ''
35 35 for token, text in reversed_tokens:
36 36
37 37 if is_token_subtype(token, Token.Name):
@@ -39,14 +39,14 b' class CompletionLexer(object):'
39 39 # Handle a trailing separator, e.g 'foo.bar.'
40 40 if current_op in self._name_separators:
41 41 if not context:
42 context.insert(0, unicode())
42 context.insert(0, '')
43 43
44 44 # Handle non-separator operators and punction.
45 45 elif current_op:
46 46 break
47 47
48 48 context.insert(0, text)
49 current_op = unicode()
49 current_op = ''
50 50
51 51 # Pygments doesn't understand that, e.g., '->' is a single operator
52 52 # in C++. This is why we have to build up an operator from
@@ -112,7 +112,7 b' class CompletionWidget(QtGui.QListWidget):'
112 112 def _update_current(self):
113 113 """ Updates the current item based on the current text.
114 114 """
115 prefix = self._current_text_cursor().selectedText()
115 prefix = self._current_text_cursor().selection().toPlainText()
116 116 if prefix:
117 117 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
118 118 QtCore.Qt.MatchCaseSensitive))
@@ -1,99 +1,14 b''
1 1 # Standard library imports
2 import re
3 2 import sys
4 3
5 4 # System library imports
6 5 from PyQt4 import QtCore, QtGui
7 6
8 7 # Local imports
8 from ansi_code_processor import QtAnsiCodeProcessor
9 9 from completion_widget import CompletionWidget
10 10
11 11
12 class AnsiCodeProcessor(object):
13 """ Translates ANSI escape codes into readable attributes.
14 """
15
16 def __init__(self):
17 self.ansi_colors = ( # Normal, Bright/Light
18 ('#000000', '#7f7f7f'), # 0: black
19 ('#cd0000', '#ff0000'), # 1: red
20 ('#00cd00', '#00ff00'), # 2: green
21 ('#cdcd00', '#ffff00'), # 3: yellow
22 ('#0000ee', '#0000ff'), # 4: blue
23 ('#cd00cd', '#ff00ff'), # 5: magenta
24 ('#00cdcd', '#00ffff'), # 6: cyan
25 ('#e5e5e5', '#ffffff')) # 7: white
26 self.reset()
27
28 def set_code(self, code):
29 """ Set attributes based on code.
30 """
31 if code == 0:
32 self.reset()
33 elif code == 1:
34 self.intensity = 1
35 self.bold = True
36 elif code == 3:
37 self.italic = True
38 elif code == 4:
39 self.underline = True
40 elif code == 22:
41 self.intensity = 0
42 self.bold = False
43 elif code == 23:
44 self.italic = False
45 elif code == 24:
46 self.underline = False
47 elif code >= 30 and code <= 37:
48 self.foreground_color = code - 30
49 elif code == 39:
50 self.foreground_color = None
51 elif code >= 40 and code <= 47:
52 self.background_color = code - 40
53 elif code == 49:
54 self.background_color = None
55
56 def reset(self):
57 """ Reset attributs to their default values.
58 """
59 self.intensity = 0
60 self.italic = False
61 self.bold = False
62 self.underline = False
63 self.foreground_color = None
64 self.background_color = None
65
66
67 class QtAnsiCodeProcessor(AnsiCodeProcessor):
68 """ Translates ANSI escape codes into QTextCharFormats.
69 """
70
71 def get_format(self):
72 """ Returns a QTextCharFormat that encodes the current style attributes.
73 """
74 format = QtGui.QTextCharFormat()
75
76 # Set foreground color
77 if self.foreground_color is not None:
78 color = self.ansi_colors[self.foreground_color][self.intensity]
79 format.setForeground(QtGui.QColor(color))
80
81 # Set background color
82 if self.background_color is not None:
83 color = self.ansi_colors[self.background_color][self.intensity]
84 format.setBackground(QtGui.QColor(color))
85
86 # Set font weight/style options
87 if self.bold:
88 format.setFontWeight(QtGui.QFont.Bold)
89 else:
90 format.setFontWeight(QtGui.QFont.Normal)
91 format.setFontItalic(self.italic)
92 format.setFontUnderline(self.underline)
93
94 return format
95
96
97 12 class ConsoleWidget(QtGui.QPlainTextEdit):
98 13 """ Base class for console-type widgets. This class is mainly concerned with
99 14 dealing with the prompt, keeping the cursor inside the editing line, and
@@ -115,7 +30,6 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
115 30 override_shortcuts = False
116 31
117 32 # Protected class variables.
118 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
119 33 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
120 34 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
121 35 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
@@ -133,14 +47,19 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
133 47 def __init__(self, parent=None):
134 48 QtGui.QPlainTextEdit.__init__(self, parent)
135 49
136 # Initialize protected variables.
50 # Initialize protected variables. Some variables contain useful state
51 # information for subclasses; they should be considered read-only.
137 52 self._ansi_processor = QtAnsiCodeProcessor()
138 53 self._completion_widget = CompletionWidget(self)
139 54 self._continuation_prompt = '> '
55 self._continuation_prompt_html = None
140 56 self._executing = False
141 57 self._prompt = ''
58 self._prompt_html = None
142 59 self._prompt_pos = 0
143 60 self._reading = False
61 self._reading_callback = None
62 self._tab_width = 8
144 63
145 64 # Set a monospaced font.
146 65 self.reset_font()
@@ -191,6 +110,11 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
191 110
192 111 self._context_menu.exec_(event.globalPos())
193 112
113 def dragMoveEvent(self, event):
114 """ Reimplemented to disable moving text by drag and drop.
115 """
116 event.ignore()
117
194 118 def keyPressEvent(self, event):
195 119 """ Reimplemented to create a console-like interface.
196 120 """
@@ -257,7 +181,10 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
257 181 else:
258 182 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
259 183 if self._reading:
184 self.appendPlainText('\n')
260 185 self._reading = False
186 if self._reading_callback:
187 self._reading_callback()
261 188 elif not self._executing:
262 189 self.execute(interactive=True)
263 190 intercepted = True
@@ -303,7 +230,8 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
303 230
304 231 # Line deletion (remove continuation prompt)
305 232 len_prompt = len(self._continuation_prompt)
306 if cursor.columnNumber() == len_prompt and \
233 if not self._reading and \
234 cursor.columnNumber() == len_prompt and \
307 235 position != self._prompt_pos:
308 236 cursor.setPosition(position - len_prompt,
309 237 QtGui.QTextCursor.KeepAnchor)
@@ -333,24 +261,38 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
333 261 # 'QPlainTextEdit' interface
334 262 #--------------------------------------------------------------------------
335 263
264 def appendHtml(self, html):
265 """ Reimplemented to not append HTML as a new paragraph, which doesn't
266 make sense for a console widget.
267 """
268 cursor = self._get_end_cursor()
269 cursor.insertHtml(html)
270
271 # After appending HTML, the text document "remembers" the current
272 # formatting, which means that subsequent calls to 'appendPlainText'
273 # will be formatted similarly, a behavior that we do not want. To
274 # prevent this, we make sure that the last character has no formatting.
275 cursor.movePosition(QtGui.QTextCursor.Left,
276 QtGui.QTextCursor.KeepAnchor)
277 if cursor.selection().toPlainText().trimmed().isEmpty():
278 # If the last character is whitespace, it doesn't matter how it's
279 # formatted, so just clear the formatting.
280 cursor.setCharFormat(QtGui.QTextCharFormat())
281 else:
282 # Otherwise, add an unformatted space.
283 cursor.movePosition(QtGui.QTextCursor.Right)
284 cursor.insertText(' ', QtGui.QTextCharFormat())
285
336 286 def appendPlainText(self, text):
337 287 """ Reimplemented to not append text as a new paragraph, which doesn't
338 288 make sense for a console widget. Also, if enabled, handle ANSI
339 289 codes.
340 290 """
341 cursor = self.textCursor()
342 cursor.movePosition(QtGui.QTextCursor.End)
343
291 cursor = self._get_end_cursor()
344 292 if self.ansi_codes:
345 format = QtGui.QTextCharFormat()
346 previous_end = 0
347 for match in self._ansi_pattern.finditer(text):
348 cursor.insertText(text[previous_end:match.start()], format)
349 previous_end = match.end()
350 for code in match.group(1).split(';'):
351 self._ansi_processor.set_code(int(code))
293 for substring in self._ansi_processor.split_string(text):
352 294 format = self._ansi_processor.get_format()
353 cursor.insertText(text[previous_end:], format)
295 cursor.insertText(substring, format)
354 296 else:
355 297 cursor.insertText(text)
356 298
@@ -358,8 +300,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
358 300 """ Reimplemented to write a new prompt. If 'keep_input' is set,
359 301 restores the old input buffer when the new prompt is written.
360 302 """
361 super(ConsoleWidget, self).clear()
362
303 QtGui.QPlainTextEdit.clear(self)
363 304 if keep_input:
364 305 input_buffer = self.input_buffer
365 306 self._show_prompt()
@@ -451,9 +392,6 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
451 392
452 393 cursor = self._get_end_cursor()
453 394 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
454
455 # Use QTextDocumentFragment intermediate object because it strips
456 # out the Unicode line break characters that Qt insists on inserting.
457 395 input_buffer = str(cursor.selection().toPlainText())
458 396
459 397 # Strip out continuation prompts.
@@ -462,16 +400,21 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
462 400 def _set_input_buffer(self, string):
463 401 """ Replaces the text in the input buffer with 'string'.
464 402 """
465 # Add continuation prompts where necessary.
466 lines = string.splitlines()
467 for i in xrange(1, len(lines)):
468 lines[i] = self._continuation_prompt + lines[i]
469 string = '\n'.join(lines)
470
471 # Replace buffer with new text.
403 # Remove old text.
472 404 cursor = self._get_end_cursor()
473 405 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
474 cursor.insertText(string)
406 cursor.removeSelectedText()
407
408 # Insert new text with continuation prompts.
409 lines = string.splitlines(True)
410 if lines:
411 self.appendPlainText(lines[0])
412 for i in xrange(1, len(lines)):
413 if self._continuation_prompt_html is None:
414 self.appendPlainText(self._continuation_prompt)
415 else:
416 self.appendHtml(self._continuation_prompt_html)
417 self.appendPlainText(lines[i])
475 418 self.moveCursor(QtGui.QTextCursor.End)
476 419
477 420 input_buffer = property(_get_input_buffer, _set_input_buffer)
@@ -484,7 +427,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
484 427 return None
485 428 cursor = self.textCursor()
486 429 if cursor.position() >= self._prompt_pos:
487 text = str(cursor.block().text())
430 text = self._get_block_plain_text(cursor.block())
488 431 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
489 432 return text[len(self._prompt):]
490 433 else:
@@ -502,6 +445,9 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
502 445 def _set_font(self, font):
503 446 """ Sets the base font for the ConsoleWidget to the specified QFont.
504 447 """
448 font_metrics = QtGui.QFontMetrics(font)
449 self.setTabStopWidth(self.tab_width * font_metrics.width(' '))
450
505 451 self._completion_widget.setFont(font)
506 452 self.document().setDefaultFont(font)
507 453
@@ -519,6 +465,21 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
519 465 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
520 466 font.setStyleHint(QtGui.QFont.TypeWriter)
521 467 self._set_font(font)
468
469 def _get_tab_width(self):
470 """ The width (in terms of space characters) for tab characters.
471 """
472 return self._tab_width
473
474 def _set_tab_width(self, tab_width):
475 """ Sets the width (in terms of space characters) for tab characters.
476 """
477 font_metrics = QtGui.QFontMetrics(self.font)
478 self.setTabStopWidth(tab_width * font_metrics.width(' '))
479
480 self._tab_width = tab_width
481
482 tab_width = property(_get_tab_width, _set_tab_width)
522 483
523 484 #---------------------------------------------------------------------------
524 485 # 'ConsoleWidget' abstract interface
@@ -569,6 +530,27 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
569 530 # 'ConsoleWidget' protected interface
570 531 #--------------------------------------------------------------------------
571 532
533 def _append_html_fetching_plain_text(self, html):
534 """ Appends 'html', then returns the plain text version of it.
535 """
536 anchor = self._get_end_cursor().position()
537 self.appendHtml(html)
538 cursor = self._get_end_cursor()
539 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
540 return str(cursor.selection().toPlainText())
541
542 def _append_plain_text_keeping_prompt(self, text):
543 """ Writes 'text' after the current prompt, then restores the old prompt
544 with its old input buffer.
545 """
546 input_buffer = self.input_buffer
547 self.appendPlainText('\n')
548 self._prompt_finished()
549
550 self.appendPlainText(text)
551 self._show_prompt()
552 self.input_buffer = input_buffer
553
572 554 def _control_down(self, modifiers):
573 555 """ Given a KeyboardModifiers flags object, return whether the Control
574 556 key is down (on Mac OS, treat the Command key as a synonym for
@@ -594,8 +576,84 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
594 576 if self.gui_completion:
595 577 self._completion_widget.show_items(cursor, items)
596 578 else:
597 text = '\n'.join(items) + '\n'
598 self._write_text_keeping_prompt(text)
579 text = self.format_as_columns(items)
580 self._append_plain_text_keeping_prompt(text)
581
582 def format_as_columns(self, items, separator=' '):
583 """ Transform a list of strings into a single string with columns.
584
585 Parameters
586 ----------
587 items : sequence [str]
588 The strings to process.
589
590 separator : str, optional [default is two spaces]
591 The string that separates columns.
592
593 Returns
594 -------
595 The formatted string.
596 """
597 # Note: this code is adapted from columnize 0.3.2.
598 # See http://code.google.com/p/pycolumnize/
599
600 font_metrics = QtGui.QFontMetrics(self.font)
601 displaywidth = max(5, (self.width() / font_metrics.width(' ')) - 1)
602
603 # Some degenerate cases
604 size = len(items)
605 if size == 0:
606 return "\n"
607 elif size == 1:
608 return '%s\n' % str(items[0])
609
610 # Try every row count from 1 upwards
611 array_index = lambda nrows, row, col: nrows*col + row
612 for nrows in range(1, size):
613 ncols = (size + nrows - 1) // nrows
614 colwidths = []
615 totwidth = -len(separator)
616 for col in range(ncols):
617 # Get max column width for this column
618 colwidth = 0
619 for row in range(nrows):
620 i = array_index(nrows, row, col)
621 if i >= size: break
622 x = items[i]
623 colwidth = max(colwidth, len(x))
624 colwidths.append(colwidth)
625 totwidth += colwidth + len(separator)
626 if totwidth > displaywidth:
627 break
628 if totwidth <= displaywidth:
629 break
630
631 # The smallest number of rows computed and the max widths for each
632 # column has been obtained. Now we just have to format each of the rows.
633 string = ''
634 for row in range(nrows):
635 texts = []
636 for col in range(ncols):
637 i = row + nrows*col
638 if i >= size:
639 texts.append('')
640 else:
641 texts.append(items[i])
642 while texts and not texts[-1]:
643 del texts[-1]
644 for col in range(len(texts)):
645 texts[col] = texts[col].ljust(colwidths[col])
646 string += "%s\n" % str(separator.join(texts))
647 return string
648
649 def _get_block_plain_text(self, block):
650 """ Given a QTextBlock, return its unformatted text.
651 """
652 cursor = QtGui.QTextCursor(block)
653 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
654 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
655 QtGui.QTextCursor.KeepAnchor)
656 return str(cursor.selection().toPlainText())
599 657
600 658 def _get_end_cursor(self):
601 659 """ Convenience method that returns a cursor for the last character.
@@ -677,6 +735,70 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
677 735 self.setReadOnly(True)
678 736 self._prompt_finished_hook()
679 737
738 def _readline(self, prompt='', callback=None):
739 """ Reads one line of input from the user.
740
741 Parameters
742 ----------
743 prompt : str, optional
744 The prompt to print before reading the line.
745
746 callback : callable, optional
747 A callback to execute with the read line. If not specified, input is
748 read *synchronously* and this method does not return until it has
749 been read.
750
751 Returns
752 -------
753 If a callback is specified, returns nothing. Otherwise, returns the
754 input string with the trailing newline stripped.
755 """
756 if self._reading:
757 raise RuntimeError('Cannot read a line. Widget is already reading.')
758
759 if not callback and not self.isVisible():
760 # If the user cannot see the widget, this function cannot return.
761 raise RuntimeError('Cannot synchronously read a line if the widget'
762 'is not visible!')
763
764 self._reading = True
765 self._show_prompt(prompt, newline=False)
766
767 if callback is None:
768 self._reading_callback = None
769 while self._reading:
770 QtCore.QCoreApplication.processEvents()
771 return self.input_buffer.rstrip('\n')
772
773 else:
774 self._reading_callback = lambda: \
775 callback(self.input_buffer.rstrip('\n'))
776
777 def _reset(self):
778 """ Clears the console and resets internal state variables.
779 """
780 QtGui.QPlainTextEdit.clear(self)
781 self._executing = self._reading = False
782
783 def _set_continuation_prompt(self, prompt, html=False):
784 """ Sets the continuation prompt.
785
786 Parameters
787 ----------
788 prompt : str
789 The prompt to show when more input is needed.
790
791 html : bool, optional (default False)
792 If set, the prompt will be inserted as formatted HTML. Otherwise,
793 the prompt will be treated as plain text, though ANSI color codes
794 will be handled.
795 """
796 if html:
797 self._continuation_prompt_html = prompt
798 else:
799 self._continuation_prompt = prompt
800 self._continuation_prompt_html = None
801
680 802 def _set_position(self, position):
681 803 """ Convenience method to set the position of the cursor.
682 804 """
@@ -689,33 +811,60 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
689 811 """
690 812 self.setTextCursor(self._get_selection_cursor(start, end))
691 813
692 def _show_prompt(self, prompt=None):
693 """ Writes a new prompt at the end of the buffer. If 'prompt' is not
694 specified, uses the previous prompt.
695 """
696 if prompt is not None:
697 self._prompt = prompt
698 self.appendPlainText('\n' + self._prompt)
814 def _show_prompt(self, prompt=None, html=False, newline=True):
815 """ Writes a new prompt at the end of the buffer.
816
817 Parameters
818 ----------
819 prompt : str, optional
820 The prompt to show. If not specified, the previous prompt is used.
821
822 html : bool, optional (default False)
823 Only relevant when a prompt is specified. If set, the prompt will
824 be inserted as formatted HTML. Otherwise, the prompt will be treated
825 as plain text, though ANSI color codes will be handled.
826
827 newline : bool, optional (default True)
828 If set, a new line will be written before showing the prompt if
829 there is not already a newline at the end of the buffer.
830 """
831 # Insert a preliminary newline, if necessary.
832 if newline:
833 cursor = self._get_end_cursor()
834 if cursor.position() > 0:
835 cursor.movePosition(QtGui.QTextCursor.Left,
836 QtGui.QTextCursor.KeepAnchor)
837 if str(cursor.selection().toPlainText()) != '\n':
838 self.appendPlainText('\n')
839
840 # Write the prompt.
841 if prompt is None:
842 if self._prompt_html is None:
843 self.appendPlainText(self._prompt)
844 else:
845 self.appendHtml(self._prompt_html)
846 else:
847 if html:
848 self._prompt = self._append_html_fetching_plain_text(prompt)
849 self._prompt_html = prompt
850 else:
851 self.appendPlainText(prompt)
852 self._prompt = prompt
853 self._prompt_html = None
854
699 855 self._prompt_pos = self._get_end_cursor().position()
700 856 self._prompt_started()
701 857
702 858 def _show_continuation_prompt(self):
703 859 """ Writes a new continuation prompt at the end of the buffer.
704 860 """
705 self.appendPlainText(self._continuation_prompt)
706 self._prompt_started()
707
708 def _write_text_keeping_prompt(self, text):
709 """ Writes 'text' after the current prompt, then restores the old prompt
710 with its old input buffer.
711 """
712 input_buffer = self.input_buffer
713 self.appendPlainText('\n')
714 self._prompt_finished()
861 if self._continuation_prompt_html is None:
862 self.appendPlainText(self._continuation_prompt)
863 else:
864 self._continuation_prompt = self._append_html_fetching_plain_text(
865 self._continuation_prompt_html)
715 866
716 self.appendPlainText(text)
717 self._show_prompt()
718 self.input_buffer = input_buffer
867 self._prompt_started()
719 868
720 869 def _in_buffer(self, position):
721 870 """ Returns whether the given position is inside the editing region.
@@ -1,5 +1,6 b''
1 1 # Standard library imports
2 2 import signal
3 import sys
3 4
4 5 # System library imports
5 6 from pygments.lexers import PythonLexer
@@ -15,12 +16,12 b' from pygments_highlighter import PygmentsHighlighter'
15 16
16 17
17 18 class FrontendHighlighter(PygmentsHighlighter):
18 """ A Python PygmentsHighlighter that can be turned on and off and which
19 knows about continuation prompts.
19 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 prompts.
20 21 """
21 22
22 23 def __init__(self, frontend):
23 PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
24 super(FrontendHighlighter, self).__init__(frontend.document())
24 25 self._current_offset = 0
25 26 self._frontend = frontend
26 27 self.highlighting_on = False
@@ -28,17 +29,32 b' class FrontendHighlighter(PygmentsHighlighter):'
28 29 def highlightBlock(self, qstring):
29 30 """ Highlight a block of text. Reimplemented to highlight selectively.
30 31 """
31 if self.highlighting_on:
32 for prompt in (self._frontend._continuation_prompt,
33 self._frontend._prompt):
34 if qstring.startsWith(prompt):
35 qstring.remove(0, len(prompt))
36 self._current_offset = len(prompt)
37 break
38 PygmentsHighlighter.highlightBlock(self, qstring)
32 if not self.highlighting_on:
33 return
34
35 # The input to this function is unicode string that may contain
36 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 # the string as plain text so we can compare it.
38 current_block = self.currentBlock()
39 string = self._frontend._get_block_plain_text(current_block)
40
41 # Decide whether to check for the regular or continuation prompt.
42 if current_block.contains(self._frontend._prompt_pos):
43 prompt = self._frontend._prompt
44 else:
45 prompt = self._frontend._continuation_prompt
46
47 # Don't highlight the part of the string that contains the prompt.
48 if string.startswith(prompt):
49 self._current_offset = len(prompt)
50 qstring.remove(0, len(prompt))
51 else:
52 self._current_offset = 0
53
54 PygmentsHighlighter.highlightBlock(self, qstring)
39 55
40 56 def setFormat(self, start, count, format):
41 """ Reimplemented to avoid highlighting continuation prompts.
57 """ Reimplemented to highlight selectively.
42 58 """
43 59 start += self._current_offset
44 60 PygmentsHighlighter.setFormat(self, start, count, format)
@@ -47,7 +63,7 b' class FrontendHighlighter(PygmentsHighlighter):'
47 63 class FrontendWidget(HistoryConsoleWidget):
48 64 """ A Qt frontend for a generic Python kernel.
49 65 """
50
66
51 67 # Emitted when an 'execute_reply' is received from the kernel.
52 68 executed = QtCore.pyqtSignal(object)
53 69
@@ -58,10 +74,6 b' class FrontendWidget(HistoryConsoleWidget):'
58 74 def __init__(self, parent=None):
59 75 super(FrontendWidget, self).__init__(parent)
60 76
61 # ConsoleWidget protected variables.
62 self._continuation_prompt = '... '
63 self._prompt = '>>> '
64
65 77 # FrontendWidget protected variables.
66 78 self._call_tip_widget = CallTipWidget(self)
67 79 self._completion_lexer = CompletionLexer(PythonLexer())
@@ -70,6 +82,10 b' class FrontendWidget(HistoryConsoleWidget):'
70 82 self._input_splitter = InputSplitter(input_mode='replace')
71 83 self._kernel_manager = None
72 84
85 # Configure the ConsoleWidget.
86 self.tab_width = 4
87 self._set_continuation_prompt('... ')
88
73 89 self.document().contentsChange.connect(self._document_contents_change)
74 90
75 91 #---------------------------------------------------------------------------
@@ -103,7 +119,7 b' class FrontendWidget(HistoryConsoleWidget):'
103 119 prompt created. When triggered by an Enter/Return key press,
104 120 'interactive' is True; otherwise, it is False.
105 121 """
106 complete = self._input_splitter.push(source)
122 complete = self._input_splitter.push(source.expandtabs(4))
107 123 if interactive:
108 124 complete = not self._input_splitter.push_accepts_more()
109 125 return complete
@@ -117,18 +133,22 b' class FrontendWidget(HistoryConsoleWidget):'
117 133 def _prompt_started_hook(self):
118 134 """ Called immediately after a new prompt is displayed.
119 135 """
120 self._highlighter.highlighting_on = True
136 if not self._reading:
137 self._highlighter.highlighting_on = True
121 138
122 # Auto-indent if this is a continuation prompt.
123 if self._get_prompt_cursor().blockNumber() != \
124 self._get_end_cursor().blockNumber():
125 self.appendPlainText(' ' * self._input_splitter.indent_spaces)
139 # Auto-indent if this is a continuation prompt.
140 if self._get_prompt_cursor().blockNumber() != \
141 self._get_end_cursor().blockNumber():
142 spaces = self._input_splitter.indent_spaces
143 self.appendPlainText('\t' * (spaces / self.tab_width))
144 self.appendPlainText(' ' * (spaces % self.tab_width))
126 145
127 146 def _prompt_finished_hook(self):
128 147 """ Called immediately after a prompt is finished, i.e. when some input
129 148 will be processed and a new prompt displayed.
130 149 """
131 self._highlighter.highlighting_on = False
150 if not self._reading:
151 self._highlighter.highlighting_on = False
132 152
133 153 def _tab_pressed(self):
134 154 """ Called when the tab key is pressed. Returns whether to continue
@@ -136,9 +156,7 b' class FrontendWidget(HistoryConsoleWidget):'
136 156 """
137 157 self._keep_cursor_in_buffer()
138 158 cursor = self.textCursor()
139 if not self._complete():
140 cursor.insertText(' ')
141 return False
159 return not self._complete()
142 160
143 161 #---------------------------------------------------------------------------
144 162 # 'FrontendWidget' interface
@@ -161,22 +179,24 b' class FrontendWidget(HistoryConsoleWidget):'
161 179 """
162 180 # Disconnect the old kernel manager, if necessary.
163 181 if self._kernel_manager is not None:
164 self._kernel_manager.started_listening.disconnect(
165 self._started_listening)
166 self._kernel_manager.stopped_listening.disconnect(
167 self._stopped_listening)
182 self._kernel_manager.started_channels.disconnect(
183 self._started_channels)
184 self._kernel_manager.stopped_channels.disconnect(
185 self._stopped_channels)
168 186
169 187 # Disconnect the old kernel manager's channels.
170 188 sub = self._kernel_manager.sub_channel
171 189 xreq = self._kernel_manager.xreq_channel
190 rep = self._kernel_manager.rep_channel
172 191 sub.message_received.disconnect(self._handle_sub)
173 192 xreq.execute_reply.disconnect(self._handle_execute_reply)
174 193 xreq.complete_reply.disconnect(self._handle_complete_reply)
175 194 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
195 rep.readline_requested.disconnect(self._handle_req)
176 196
177 197 # Handle the case where the old kernel manager is still listening.
178 198 if self._kernel_manager.channels_running:
179 self._stopped_listening()
199 self._stopped_channels()
180 200
181 201 # Set the new kernel manager.
182 202 self._kernel_manager = kernel_manager
@@ -184,21 +204,23 b' class FrontendWidget(HistoryConsoleWidget):'
184 204 return
185 205
186 206 # Connect the new kernel manager.
187 kernel_manager.started_listening.connect(self._started_listening)
188 kernel_manager.stopped_listening.connect(self._stopped_listening)
207 kernel_manager.started_channels.connect(self._started_channels)
208 kernel_manager.stopped_channels.connect(self._stopped_channels)
189 209
190 210 # Connect the new kernel manager's channels.
191 211 sub = kernel_manager.sub_channel
192 212 xreq = kernel_manager.xreq_channel
213 rep = kernel_manager.rep_channel
193 214 sub.message_received.connect(self._handle_sub)
194 215 xreq.execute_reply.connect(self._handle_execute_reply)
195 216 xreq.complete_reply.connect(self._handle_complete_reply)
196 217 xreq.object_info_reply.connect(self._handle_object_info_reply)
218 rep.readline_requested.connect(self._handle_req)
197 219
198 # Handle the case where the kernel manager started listening before
220 # Handle the case where the kernel manager started channels before
199 221 # we connected.
200 222 if kernel_manager.channels_running:
201 self._started_listening()
223 self._started_channels()
202 224
203 225 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
204 226
@@ -240,6 +262,13 b' class FrontendWidget(HistoryConsoleWidget):'
240 262 self._complete_pos = self.textCursor().position()
241 263 return True
242 264
265 def _get_banner(self):
266 """ Gets a banner to display at the beginning of a session.
267 """
268 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
269 '"license" for more information.'
270 return banner % (sys.version, sys.platform)
271
243 272 def _get_context(self, cursor=None):
244 273 """ Gets the context at the current cursor location.
245 274 """
@@ -247,7 +276,7 b' class FrontendWidget(HistoryConsoleWidget):'
247 276 cursor = self.textCursor()
248 277 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
249 278 QtGui.QTextCursor.KeepAnchor)
250 text = unicode(cursor.selectedText())
279 text = str(cursor.selection().toPlainText())
251 280 return self._completion_lexer.get_context(text)
252 281
253 282 def _interrupt_kernel(self):
@@ -259,8 +288,26 b' class FrontendWidget(HistoryConsoleWidget):'
259 288 self.appendPlainText('Kernel process is either remote or '
260 289 'unspecified. Cannot interrupt.\n')
261 290
291 def _show_interpreter_prompt(self):
292 """ Shows a prompt for the interpreter.
293 """
294 self._show_prompt('>>> ')
295
262 296 #------ Signal handlers ----------------------------------------------------
263 297
298 def _started_channels(self):
299 """ Called when the kernel manager has started listening.
300 """
301 self._reset()
302 self.appendPlainText(self._get_banner())
303 self._show_interpreter_prompt()
304
305 def _stopped_channels(self):
306 """ Called when the kernel manager has stopped listening.
307 """
308 # FIXME: Print a message here?
309 pass
310
264 311 def _document_contents_change(self, position, removed, added):
265 312 """ Called whenever the document's content changes. Display a calltip
266 313 if appropriate.
@@ -272,6 +319,15 b' class FrontendWidget(HistoryConsoleWidget):'
272 319 if position == self.textCursor().position():
273 320 self._call_tip()
274 321
322 def _handle_req(self, req):
323 # Make sure that all output from the SUB channel has been processed
324 # before entering readline mode.
325 self.kernel_manager.sub_channel.flush()
326
327 def callback(line):
328 self.kernel_manager.rep_channel.readline(line)
329 self._readline(callback=callback)
330
275 331 def _handle_sub(self, omsg):
276 332 if self._hidden:
277 333 return
@@ -280,15 +336,13 b' class FrontendWidget(HistoryConsoleWidget):'
280 336 handler(omsg)
281 337
282 338 def _handle_pyout(self, omsg):
283 session = omsg['parent_header']['session']
284 if session == self.kernel_manager.session.session:
285 self.appendPlainText(omsg['content']['data'] + '\n')
339 self.appendPlainText(omsg['content']['data'] + '\n')
286 340
287 341 def _handle_stream(self, omsg):
288 342 self.appendPlainText(omsg['content']['data'])
289 343 self.moveCursor(QtGui.QTextCursor.End)
290 344
291 def _handle_execute_reply(self, rep):
345 def _handle_execute_reply(self, reply):
292 346 if self._hidden:
293 347 return
294 348
@@ -296,16 +350,20 b' class FrontendWidget(HistoryConsoleWidget):'
296 350 # before writing a new prompt.
297 351 self.kernel_manager.sub_channel.flush()
298 352
299 content = rep['content']
300 status = content['status']
353 status = reply['content']['status']
301 354 if status == 'error':
302 self.appendPlainText(content['traceback'][-1])
355 self._handle_execute_error(reply)
303 356 elif status == 'aborted':
304 357 text = "ERROR: ABORTED\n"
305 358 self.appendPlainText(text)
306 359 self._hidden = True
307 self._show_prompt()
308 self.executed.emit(rep)
360 self._show_interpreter_prompt()
361 self.executed.emit(reply)
362
363 def _handle_execute_error(self, reply):
364 content = reply['content']
365 traceback = ''.join(content['traceback'])
366 self.appendPlainText(traceback)
309 367
310 368 def _handle_complete_reply(self, rep):
311 369 cursor = self.textCursor()
@@ -322,9 +380,3 b' class FrontendWidget(HistoryConsoleWidget):'
322 380 doc = rep['content']['docstring']
323 381 if doc:
324 382 self._call_tip_widget.show_docstring(doc)
325
326 def _started_listening(self):
327 self.clear()
328
329 def _stopped_listening(self):
330 pass
@@ -2,6 +2,7 b''
2 2 from PyQt4 import QtCore, QtGui
3 3
4 4 # Local imports
5 from IPython.core.usage import default_banner
5 6 from frontend_widget import FrontendWidget
6 7
7 8
@@ -9,6 +10,15 b' class IPythonWidget(FrontendWidget):'
9 10 """ A FrontendWidget for an IPython kernel.
10 11 """
11 12
13 # The default stylesheet for prompts, colors, etc.
14 default_stylesheet = """
15 .error { color: red; }
16 .in-prompt { color: navy; }
17 .in-prompt-number { font-weight: bold; }
18 .out-prompt { color: darkred; }
19 .out-prompt-number { font-weight: bold; }
20 """
21
12 22 #---------------------------------------------------------------------------
13 23 # 'QObject' interface
14 24 #---------------------------------------------------------------------------
@@ -16,7 +26,12 b' class IPythonWidget(FrontendWidget):'
16 26 def __init__(self, parent=None):
17 27 super(IPythonWidget, self).__init__(parent)
18 28
29 # Initialize protected variables.
19 30 self._magic_overrides = {}
31 self._prompt_count = 0
32
33 # Set a default stylesheet.
34 self.set_style_sheet(self.default_stylesheet)
20 35
21 36 #---------------------------------------------------------------------------
22 37 # 'ConsoleWidget' abstract interface
@@ -37,7 +52,7 b' class IPythonWidget(FrontendWidget):'
37 52 output = callback(arguments)
38 53 if output:
39 54 self.appendPlainText(output)
40 self._show_prompt()
55 self._show_interpreter_prompt()
41 56 else:
42 57 super(IPythonWidget, self)._execute(source, hidden)
43 58
@@ -51,6 +66,56 b' class IPythonWidget(FrontendWidget):'
51 66 self.execute('run %s' % path, hidden=hidden)
52 67
53 68 #---------------------------------------------------------------------------
69 # 'FrontendWidget' protected interface
70 #---------------------------------------------------------------------------
71
72 def _get_banner(self):
73 """ Reimplemented to return IPython's default banner.
74 """
75 return default_banner
76
77 def _show_interpreter_prompt(self):
78 """ Reimplemented for IPython-style prompts.
79 """
80 self._prompt_count += 1
81 prompt_template = '<span class="in-prompt">%s</span>'
82 prompt_body = '<br/>In [<span class="in-prompt-number">%i</span>]: '
83 prompt = (prompt_template % prompt_body) % self._prompt_count
84 self._show_prompt(prompt, html=True)
85
86 # Update continuation prompt to reflect (possibly) new prompt length.
87 cont_prompt_chars = '...: '
88 space_count = len(self._prompt.lstrip()) - len(cont_prompt_chars)
89 cont_prompt_body = '&nbsp;' * space_count + cont_prompt_chars
90 self._continuation_prompt_html = prompt_template % cont_prompt_body
91
92 #------ Signal handlers ----------------------------------------------------
93
94 def _handle_execute_error(self, reply):
95 """ Reimplemented for IPython-style traceback formatting.
96 """
97 content = reply['content']
98 traceback_lines = content['traceback'][:]
99 traceback = ''.join(traceback_lines)
100 traceback = traceback.replace(' ', '&nbsp;')
101 traceback = traceback.replace('\n', '<br/>')
102
103 ename = content['ename']
104 ename_styled = '<span class="error">%s</span>' % ename
105 traceback = traceback.replace(ename, ename_styled)
106
107 self.appendHtml(traceback)
108
109 def _handle_pyout(self, omsg):
110 """ Reimplemented for IPython-style "display hook".
111 """
112 prompt_template = '<span class="out-prompt">%s</span>'
113 prompt_body = 'Out[<span class="out-prompt-number">%i</span>]: '
114 prompt = (prompt_template % prompt_body) % self._prompt_count
115 self.appendHtml(prompt)
116 self.appendPlainText(omsg['content']['data'] + '\n')
117
118 #---------------------------------------------------------------------------
54 119 # 'IPythonWidget' interface
55 120 #---------------------------------------------------------------------------
56 121
@@ -71,32 +136,26 b' class IPythonWidget(FrontendWidget):'
71 136 except KeyError:
72 137 pass
73 138
139 def set_style_sheet(self, stylesheet):
140 """ Sets the style sheet.
141 """
142 self.document().setDefaultStyleSheet(stylesheet)
143
74 144
75 145 if __name__ == '__main__':
76 import signal
77 146 from IPython.frontend.qt.kernelmanager import QtKernelManager
78 147
148 # Don't let Qt or ZMQ swallow KeyboardInterupts.
149 import signal
150 signal.signal(signal.SIGINT, signal.SIG_DFL)
151
79 152 # Create a KernelManager.
80 153 kernel_manager = QtKernelManager()
81 154 kernel_manager.start_kernel()
82 155 kernel_manager.start_channels()
83 156
84 # Don't let Qt or ZMQ swallow KeyboardInterupts.
85 # FIXME: Gah, ZMQ swallows even custom signal handlers. So for now we leave
86 # behind a kernel process when Ctrl-C is pressed.
87 #def sigint_hook(signum, frame):
88 # QtGui.qApp.quit()
89 #signal.signal(signal.SIGINT, sigint_hook)
90 signal.signal(signal.SIGINT, signal.SIG_DFL)
91
92 # Create the application, making sure to clean up nicely when we exit.
93 app = QtGui.QApplication([])
94 def quit_hook():
95 kernel_manager.stop_channels()
96 kernel_manager.kill_kernel()
97 app.aboutToQuit.connect(quit_hook)
98
99 157 # Launch the application.
158 app = QtGui.QApplication([])
100 159 widget = IPythonWidget()
101 160 widget.kernel_manager = kernel_manager
102 161 widget.setWindowTitle('Python')
@@ -1,7 +1,7 b''
1 1 # System library imports.
2 2 from PyQt4 import QtGui
3 3 from pygments.lexer import RegexLexer, _TokenType, Text, Error
4 from pygments.lexers import CLexer, CppLexer, PythonLexer
4 from pygments.lexers import PythonLexer
5 5 from pygments.styles.default import DefaultStyle
6 6 from pygments.token import Comment
7 7
@@ -133,7 +133,7 b' class PygmentsHighlighter(QtGui.QSyntaxHighlighter):'
133 133 if token in self._formats:
134 134 return self._formats[token]
135 135 result = None
136 for key, value in self._style.style_for_token(token) .items():
136 for key, value in self._style.style_for_token(token).items():
137 137 if value:
138 138 if result is None:
139 139 result = QtGui.QTextCharFormat()
@@ -171,12 +171,11 b' class PygmentsHighlighter(QtGui.QSyntaxHighlighter):'
171 171 qcolor = self._get_color(color)
172 172 result = QtGui.QBrush(qcolor)
173 173 self._brushes[color] = result
174
175 174 return result
176 175
177 176 def _get_color(self, color):
178 177 qcolor = QtGui.QColor()
179 qcolor.setRgb(int(color[:2],base=16),
178 qcolor.setRgb(int(color[:2], base=16),
180 179 int(color[2:4], base=16),
181 180 int(color[4:6], base=16))
182 181 return qcolor
@@ -17,6 +17,9 b' from util import MetaQObjectHasTraits'
17 17 # * QtCore.QObject.__init__ takes 1 argument, the parent. So if you are going
18 18 # to use super, any class that comes before QObject must pass it something
19 19 # reasonable.
20 # In summary, I don't think using super in these situations will work.
21 # Instead we will need to call the __init__ methods of both parents
22 # by hand. Not pretty, but it works.
20 23
21 24 class QtSubSocketChannel(SubSocketChannel, QtCore.QObject):
22 25
@@ -102,6 +105,12 b' class QtXReqSocketChannel(XReqSocketChannel, QtCore.QObject):'
102 105
103 106 class QtRepSocketChannel(RepSocketChannel, QtCore.QObject):
104 107
108 # Emitted when any message is received.
109 message_received = QtCore.pyqtSignal(object)
110
111 # Emitted when a readline request is received.
112 readline_requested = QtCore.pyqtSignal(object)
113
105 114 #---------------------------------------------------------------------------
106 115 # 'object' interface
107 116 #---------------------------------------------------------------------------
@@ -112,6 +121,22 b' class QtRepSocketChannel(RepSocketChannel, QtCore.QObject):'
112 121 QtCore.QObject.__init__(self)
113 122 RepSocketChannel.__init__(self, *args, **kw)
114 123
124 #---------------------------------------------------------------------------
125 # 'RepSocketChannel' interface
126 #---------------------------------------------------------------------------
127
128 def call_handlers(self, msg):
129 """ Reimplemented to emit signals instead of making callbacks.
130 """
131 # Emit the generic signal.
132 self.message_received.emit(msg)
133
134 # Emit signals for specialized message types.
135 msg_type = msg['msg_type']
136 if msg_type == 'readline_request':
137 self.readline_requested.emit(msg)
138
139
115 140 class QtKernelManager(KernelManager, QtCore.QObject):
116 141 """ A KernelManager that provides signals and slots.
117 142 """
@@ -119,10 +144,10 b' class QtKernelManager(KernelManager, QtCore.QObject):'
119 144 __metaclass__ = MetaQObjectHasTraits
120 145
121 146 # Emitted when the kernel manager has started listening.
122 started_listening = QtCore.pyqtSignal()
147 started_channels = QtCore.pyqtSignal()
123 148
124 149 # Emitted when the kernel manager has stopped listening.
125 stopped_listening = QtCore.pyqtSignal()
150 stopped_channels = QtCore.pyqtSignal()
126 151
127 152 # Use Qt-specific channel classes that emit signals.
128 153 sub_channel_class = QtSubSocketChannel
@@ -134,6 +159,16 b' class QtKernelManager(KernelManager, QtCore.QObject):'
134 159 KernelManager.__init__(self, *args, **kw)
135 160
136 161 #---------------------------------------------------------------------------
162 # 'object' interface
163 #---------------------------------------------------------------------------
164
165 def __init__(self, *args, **kw):
166 """ Reimplemented to ensure that QtCore.QObject is initialized first.
167 """
168 QtCore.QObject.__init__(self)
169 KernelManager.__init__(self, *args, **kw)
170
171 #---------------------------------------------------------------------------
137 172 # 'KernelManager' interface
138 173 #---------------------------------------------------------------------------
139 174
@@ -141,10 +176,10 b' class QtKernelManager(KernelManager, QtCore.QObject):'
141 176 """ Reimplemented to emit signal.
142 177 """
143 178 super(QtKernelManager, self).start_channels()
144 self.started_listening.emit()
179 self.started_channels.emit()
145 180
146 181 def stop_channels(self):
147 182 """ Reimplemented to emit signal.
148 183 """
149 184 super(QtKernelManager, self).stop_channels()
150 self.stopped_listening.emit()
185 self.stopped_channels.emit()
@@ -11,7 +11,7 b' from IPython.utils.traitlets import HasTraits'
11 11 MetaHasTraits = type(HasTraits)
12 12 MetaQObject = type(QtCore.QObject)
13 13
14 # You can switch the order of the parents here.
14 # You can switch the order of the parents here and it doesn't seem to matter.
15 15 class MetaQObjectHasTraits(MetaQObject, MetaHasTraits):
16 16 """ A metaclass that inherits from the metaclasses of both HasTraits and
17 17 QObject.
@@ -19,9 +19,4 b' class MetaQObjectHasTraits(MetaQObject, MetaHasTraits):'
19 19 Using this metaclass allows a class to inherit from both HasTraits and
20 20 QObject. See QtKernelManager for an example.
21 21 """
22 # pass
23 # ???You can get rid of this, but only if the order above is MetaQObject, MetaHasTraits
24 # def __init__(cls, name, bases, dct):
25 # MetaQObject.__init__(cls, name, bases, dct)
26 # MetaHasTraits.__init__(cls, name, bases, dct)
27
22 pass
@@ -11,12 +11,19 b' Things to do:'
11 11 * Implement event loop and poll version.
12 12 """
13 13
14 #-----------------------------------------------------------------------------
15 # Imports
16 #-----------------------------------------------------------------------------
17
14 18 # Standard library imports.
15 19 import __builtin__
20 from code import CommandCompiler
21 from cStringIO import StringIO
22 import os
16 23 import sys
24 from threading import Thread
17 25 import time
18 26 import traceback
19 from code import CommandCompiler
20 27
21 28 # System library imports.
22 29 import zmq
@@ -26,18 +33,109 b' from IPython.external.argparse import ArgumentParser'
26 33 from session import Session, Message, extract_header
27 34 from completer import KernelCompleter
28 35
36 #-----------------------------------------------------------------------------
37 # Kernel and stream classes
38 #-----------------------------------------------------------------------------
39
40 class InStream(object):
41 """ A file like object that reads from a 0MQ XREQ socket."""
42
43 def __init__(self, session, socket):
44 self.session = session
45 self.socket = socket
46
47 def close(self):
48 self.socket = None
49
50 def flush(self):
51 if self.socket is None:
52 raise ValueError('I/O operation on closed file')
53
54 def isatty(self):
55 return False
56
57 def next(self):
58 raise IOError('Seek not supported.')
59
60 def read(self, size=-1):
61 # FIXME: Do we want another request for this?
62 string = '\n'.join(self.readlines())
63 return self._truncate(string, size)
64
65 def readline(self, size=-1):
66 if self.socket is None:
67 raise ValueError('I/O operation on closed file')
68 else:
69 content = dict(size=size)
70 msg = self.session.msg('readline_request', content=content)
71 reply = self._request(msg)
72 line = reply['content']['line']
73 return self._truncate(line, size)
74
75 def readlines(self, sizehint=-1):
76 # Sizehint is ignored, as is permitted.
77 if self.socket is None:
78 raise ValueError('I/O operation on closed file')
79 else:
80 lines = []
81 while True:
82 line = self.readline()
83 if line:
84 lines.append(line)
85 else:
86 break
87 return lines
88
89 def seek(self, offset, whence=None):
90 raise IOError('Seek not supported.')
91
92 def write(self, string):
93 raise IOError('Write not supported on a read only stream.')
94
95 def writelines(self, sequence):
96 raise IOError('Write not supported on a read only stream.')
97
98 def _request(self, msg):
99 # Flush output before making the request. This ensures, for example,
100 # that raw_input(prompt) actually gets a prompt written.
101 sys.stderr.flush()
102 sys.stdout.flush()
103
104 self.socket.send_json(msg)
105 while True:
106 try:
107 reply = self.socket.recv_json(zmq.NOBLOCK)
108 except zmq.ZMQError, e:
109 if e.errno == zmq.EAGAIN:
110 pass
111 else:
112 raise
113 else:
114 break
115 return reply
116
117 def _truncate(self, string, size):
118 if size >= 0:
119 if isinstance(string, str):
120 return string[:size]
121 elif isinstance(string, unicode):
122 encoded = string.encode('utf-8')[:size]
123 return encoded.decode('utf-8', 'ignore')
124 return string
125
29 126
30 127 class OutStream(object):
31 128 """A file like object that publishes the stream to a 0MQ PUB socket."""
32 129
33 def __init__(self, session, pub_socket, name, max_buffer=200):
130 # The time interval between automatic flushes, in seconds.
131 flush_interval = 0.05
132
133 def __init__(self, session, pub_socket, name):
34 134 self.session = session
35 135 self.pub_socket = pub_socket
36 136 self.name = name
37 self._buffer = []
38 self._buffer_len = 0
39 self.max_buffer = max_buffer
40 137 self.parent_header = {}
138 self._new_buffer()
41 139
42 140 def set_parent(self, parent):
43 141 self.parent_header = extract_header(parent)
@@ -49,47 +147,50 b' class OutStream(object):'
49 147 if self.pub_socket is None:
50 148 raise ValueError(u'I/O operation on closed file')
51 149 else:
52 if self._buffer:
53 data = ''.join(self._buffer)
150 data = self._buffer.getvalue()
151 if data:
54 152 content = {u'name':self.name, u'data':data}
55 153 msg = self.session.msg(u'stream', content=content,
56 154 parent=self.parent_header)
57 155 print>>sys.__stdout__, Message(msg)
58 156 self.pub_socket.send_json(msg)
59 self._buffer_len = 0
60 self._buffer = []
157
158 self._buffer.close()
159 self._new_buffer()
61 160
62 def isattr(self):
161 def isatty(self):
63 162 return False
64 163
65 164 def next(self):
66 165 raise IOError('Read not supported on a write only stream.')
67 166
68 def read(self, size=None):
167 def read(self, size=-1):
69 168 raise IOError('Read not supported on a write only stream.')
70 169
71 readline=read
170 def readline(self, size=-1):
171 raise IOError('Read not supported on a write only stream.')
72 172
73 def write(self, s):
173 def write(self, string):
74 174 if self.pub_socket is None:
75 175 raise ValueError('I/O operation on closed file')
76 176 else:
77 self._buffer.append(s)
78 self._buffer_len += len(s)
79 self._maybe_send()
80
81 def _maybe_send(self):
82 if '\n' in self._buffer[-1]:
83 self.flush()
84 if self._buffer_len > self.max_buffer:
85 self.flush()
177 self._buffer.write(string)
178 current_time = time.time()
179 if self._start <= 0:
180 self._start = current_time
181 elif current_time - self._start > self.flush_interval:
182 self.flush()
86 183
87 184 def writelines(self, sequence):
88 185 if self.pub_socket is None:
89 186 raise ValueError('I/O operation on closed file')
90 187 else:
91 for s in sequence:
92 self.write(s)
188 for string in sequence:
189 self.write(string)
190
191 def _new_buffer(self):
192 self._buffer = StringIO()
193 self._start = -1
93 194
94 195
95 196 class DisplayHook(object):
@@ -112,28 +213,6 b' class DisplayHook(object):'
112 213 self.parent_header = extract_header(parent)
113 214
114 215
115 class RawInput(object):
116
117 def __init__(self, session, socket):
118 self.session = session
119 self.socket = socket
120
121 def __call__(self, prompt=None):
122 msg = self.session.msg(u'raw_input')
123 self.socket.send_json(msg)
124 while True:
125 try:
126 reply = self.socket.recv_json(zmq.NOBLOCK)
127 except zmq.ZMQError, e:
128 if e.errno == zmq.EAGAIN:
129 pass
130 else:
131 raise
132 else:
133 break
134 return reply[u'content'][u'data']
135
136
137 216 class Kernel(object):
138 217
139 218 def __init__(self, session, reply_socket, pub_socket):
@@ -183,6 +262,7 b' class Kernel(object):'
183 262 return
184 263 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
185 264 self.pub_socket.send_json(pyin_msg)
265
186 266 try:
187 267 comp_code = self.compiler(code, '<zmq-kernel>')
188 268 sys.displayhook.set_parent(parent)
@@ -194,7 +274,7 b' class Kernel(object):'
194 274 exc_content = {
195 275 u'status' : u'error',
196 276 u'traceback' : tb,
197 u'etype' : unicode(etype),
277 u'ename' : unicode(etype.__name__),
198 278 u'evalue' : unicode(evalue)
199 279 }
200 280 exc_msg = self.session.msg(u'pyerr', exc_content, parent)
@@ -202,6 +282,12 b' class Kernel(object):'
202 282 reply_content = exc_content
203 283 else:
204 284 reply_content = {'status' : 'ok'}
285
286 # Flush output before sending the reply.
287 sys.stderr.flush()
288 sys.stdout.flush()
289
290 # Send the reply.
205 291 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
206 292 print>>sys.__stdout__, Message(reply_msg)
207 293 self.reply_socket.send(ident, zmq.SNDMORE)
@@ -270,19 +356,62 b' class Kernel(object):'
270 356 else:
271 357 handler(ident, omsg)
272 358
359 #-----------------------------------------------------------------------------
360 # Kernel main and launch functions
361 #-----------------------------------------------------------------------------
362
363 class ExitPollerUnix(Thread):
364 """ A Unix-specific daemon thread that terminates the program immediately
365 when this process' parent process no longer exists.
366 """
367
368 def __init__(self):
369 super(ExitPollerUnix, self).__init__()
370 self.daemon = True
371
372 def run(self):
373 # We cannot use os.waitpid because it works only for child processes.
374 from errno import EINTR
375 while True:
376 try:
377 if os.getppid() == 1:
378 os._exit(1)
379 time.sleep(1.0)
380 except OSError, e:
381 if e.errno == EINTR:
382 continue
383 raise
384
385 class ExitPollerWindows(Thread):
386 """ A Windows-specific daemon thread that terminates the program immediately
387 when a Win32 handle is signaled.
388 """
389
390 def __init__(self, handle):
391 super(ExitPollerWindows, self).__init__()
392 self.daemon = True
393 self.handle = handle
394
395 def run(self):
396 from _subprocess import WaitForSingleObject, WAIT_OBJECT_0, INFINITE
397 result = WaitForSingleObject(self.handle, INFINITE)
398 if result == WAIT_OBJECT_0:
399 os._exit(1)
400
273 401
274 402 def bind_port(socket, ip, port):
275 403 """ Binds the specified ZMQ socket. If the port is less than zero, a random
276 404 port is chosen. Returns the port that was bound.
277 405 """
278 406 connection = 'tcp://%s' % ip
279 if port < 0:
407 if port <= 0:
280 408 port = socket.bind_to_random_port(connection)
281 409 else:
282 410 connection += ':%i' % port
283 411 socket.bind(connection)
284 412 return port
285 413
414
286 415 def main():
287 416 """ Main entry point for launching a kernel.
288 417 """
@@ -291,12 +420,21 b' def main():'
291 420 parser.add_argument('--ip', type=str, default='127.0.0.1',
292 421 help='set the kernel\'s IP address [default: local]')
293 422 parser.add_argument('--xrep', type=int, metavar='PORT', default=0,
294 help='set the XREP Channel port [default: random]')
423 help='set the XREP channel port [default: random]')
295 424 parser.add_argument('--pub', type=int, metavar='PORT', default=0,
296 help='set the PUB Channel port [default: random]')
425 help='set the PUB channel port [default: random]')
426 parser.add_argument('--req', type=int, metavar='PORT', default=0,
427 help='set the REQ channel port [default: random]')
428 if sys.platform == 'win32':
429 parser.add_argument('--parent', type=int, metavar='HANDLE',
430 default=0, help='kill this process if the process '
431 'with HANDLE dies')
432 else:
433 parser.add_argument('--parent', action='store_true',
434 help='kill this process if its parent dies')
297 435 namespace = parser.parse_args()
298 436
299 # Create context, session, and kernel sockets.
437 # Create a context, a session, and the kernel sockets.
300 438 print >>sys.__stdout__, "Starting the kernel..."
301 439 context = zmq.Context()
302 440 session = Session(username=u'kernel')
@@ -309,34 +447,63 b' def main():'
309 447 pub_port = bind_port(pub_socket, namespace.ip, namespace.pub)
310 448 print >>sys.__stdout__, "PUB Channel on port", pub_port
311 449
450 req_socket = context.socket(zmq.XREQ)
451 req_port = bind_port(req_socket, namespace.ip, namespace.req)
452 print >>sys.__stdout__, "REQ Channel on port", req_port
453
312 454 # Redirect input streams and set a display hook.
455 sys.stdin = InStream(session, req_socket)
313 456 sys.stdout = OutStream(session, pub_socket, u'stdout')
314 457 sys.stderr = OutStream(session, pub_socket, u'stderr')
315 458 sys.displayhook = DisplayHook(session, pub_socket)
316 459
460 # Create the kernel.
317 461 kernel = Kernel(session, reply_socket, pub_socket)
318 462
319 # For debugging convenience, put sleep and a string in the namespace, so we
320 # have them every time we start.
321 kernel.user_ns['sleep'] = time.sleep
322 kernel.user_ns['s'] = 'Test string'
323
324 print >>sys.__stdout__, "Use Ctrl-\\ (NOT Ctrl-C!) to terminate."
463 # Configure this kernel/process to die on parent termination, if necessary.
464 if namespace.parent:
465 if sys.platform == 'win32':
466 poller = ExitPollerWindows(namespace.parent)
467 else:
468 poller = ExitPollerUnix()
469 poller.start()
470
471 # Start the kernel mainloop.
325 472 kernel.start()
326 473
327 def launch_kernel(xrep_port=0, pub_port=0):
328 """ Launches a localhost kernel, binding to the specified ports. For any
329 port that is left unspecified, a port is chosen by the operating system.
330 474
331 Returns a tuple of form:
332 (kernel_process [Popen], rep_port [int], sub_port [int])
475 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, independent=False):
476 """ Launches a localhost kernel, binding to the specified ports.
477
478 Parameters
479 ----------
480 xrep_port : int, optional
481 The port to use for XREP channel.
482
483 pub_port : int, optional
484 The port to use for the SUB channel.
485
486 req_port : int, optional
487 The port to use for the REQ (raw input) channel.
488
489 independent : bool, optional (default False)
490 If set, the kernel process is guaranteed to survive if this process
491 dies. If not set, an effort is made to ensure that the kernel is killed
492 when this process dies. Note that in this case it is still good practice
493 to kill kernels manually before exiting.
494
495 Returns
496 -------
497 A tuple of form:
498 (kernel_process, xrep_port, pub_port, req_port)
499 where kernel_process is a Popen object and the ports are integers.
333 500 """
334 501 import socket
335 502 from subprocess import Popen
336 503
337 504 # Find open ports as necessary.
338 505 ports = []
339 ports_needed = int(xrep_port == 0) + int(pub_port == 0)
506 ports_needed = int(xrep_port <= 0) + int(pub_port <= 0) + int(req_port <= 0)
340 507 for i in xrange(ports_needed):
341 508 sock = socket.socket()
342 509 sock.bind(('', 0))
@@ -345,16 +512,35 b' def launch_kernel(xrep_port=0, pub_port=0):'
345 512 port = sock.getsockname()[1]
346 513 sock.close()
347 514 ports[i] = port
348 if xrep_port == 0:
349 xrep_port = ports.pop()
350 if pub_port == 0:
351 pub_port = ports.pop()
515 if xrep_port <= 0:
516 xrep_port = ports.pop(0)
517 if pub_port <= 0:
518 pub_port = ports.pop(0)
519 if req_port <= 0:
520 req_port = ports.pop(0)
352 521
353 522 # Spawn a kernel.
354 523 command = 'from IPython.zmq.kernel import main; main()'
355 proc = Popen([ sys.executable, '-c', command,
356 '--xrep', str(xrep_port), '--pub', str(pub_port) ])
357 return proc, xrep_port, pub_port
524 arguments = [ sys.executable, '-c', command, '--xrep', str(xrep_port),
525 '--pub', str(pub_port), '--req', str(req_port) ]
526 if independent:
527 if sys.platform == 'win32':
528 proc = Popen(['start', '/b'] + arguments, shell=True)
529 else:
530 proc = Popen(arguments, preexec_fn=lambda: os.setsid())
531 else:
532 if sys.platform == 'win32':
533 from _subprocess import DuplicateHandle, GetCurrentProcess, \
534 DUPLICATE_SAME_ACCESS
535 pid = GetCurrentProcess()
536 handle = DuplicateHandle(pid, pid, pid, 0,
537 True, # Inheritable by new processes.
538 DUPLICATE_SAME_ACCESS)
539 proc = Popen(arguments + ['--parent', str(int(handle))])
540 else:
541 proc = Popen(arguments + ['--parent'])
542
543 return proc, xrep_port, pub_port, req_port
358 544
359 545
360 546 if __name__ == '__main__':
@@ -74,7 +74,8 b' class ZmqSocketChannel(Thread):'
74 74 self.context = context
75 75 self.session = session
76 76 if address[1] == 0:
77 raise InvalidPortNumber('The port number for a channel cannot be 0.')
77 message = 'The port number for a channel cannot be 0.'
78 raise InvalidPortNumber(message)
78 79 self._address = address
79 80
80 81 def stop(self):
@@ -198,7 +199,6 b' class XReqSocketChannel(ZmqSocketChannel):'
198 199 Returns
199 200 -------
200 201 The msg_id of the message sent.
201
202 202 """
203 203 content = dict(text=text, line=line)
204 204 msg = self.session.msg('complete_request', content)
@@ -217,7 +217,6 b' class XReqSocketChannel(ZmqSocketChannel):'
217 217 -------
218 218 The msg_id of the message sent.
219 219 """
220 print oname
221 220 content = dict(oname=oname)
222 221 msg = self.session.msg('object_info_request', content)
223 222 self._queue_request(msg)
@@ -338,15 +337,84 b' class SubSocketChannel(ZmqSocketChannel):'
338 337 class RepSocketChannel(ZmqSocketChannel):
339 338 """A reply channel to handle raw_input requests that the kernel makes."""
340 339
341 def on_raw_input(self):
342 pass
340 msg_queue = None
341
342 def __init__(self, context, session, address):
343 self.msg_queue = Queue()
344 super(RepSocketChannel, self).__init__(context, session, address)
345
346 def run(self):
347 """The thread's main activity. Call start() instead."""
348 self.socket = self.context.socket(zmq.XREQ)
349 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
350 self.socket.connect('tcp://%s:%i' % self.address)
351 self.ioloop = ioloop.IOLoop()
352 self.iostate = POLLERR|POLLIN
353 self.ioloop.add_handler(self.socket, self._handle_events,
354 self.iostate)
355 self.ioloop.start()
356
357 def stop(self):
358 self.ioloop.stop()
359 super(RepSocketChannel, self).stop()
360
361 def call_handlers(self, msg):
362 """This method is called in the ioloop thread when a message arrives.
363
364 Subclasses should override this method to handle incoming messages.
365 It is important to remember that this method is called in the thread
366 so that some logic must be done to ensure that the application leve
367 handlers are called in the application thread.
368 """
369 raise NotImplementedError('call_handlers must be defined in a subclass.')
370
371 def readline(self, line):
372 """A send a line of raw input to the kernel.
373
374 Parameters
375 ----------
376 line : str
377 The line of the input.
378 """
379 content = dict(line=line)
380 msg = self.session.msg('readline_reply', content)
381 self._queue_reply(msg)
382
383 def _handle_events(self, socket, events):
384 if events & POLLERR:
385 self._handle_err()
386 if events & POLLOUT:
387 self._handle_send()
388 if events & POLLIN:
389 self._handle_recv()
390
391 def _handle_recv(self):
392 msg = self.socket.recv_json()
393 self.call_handlers(msg)
394
395 def _handle_send(self):
396 try:
397 msg = self.msg_queue.get(False)
398 except Empty:
399 pass
400 else:
401 self.socket.send_json(msg)
402 if self.msg_queue.empty():
403 self.drop_io_state(POLLOUT)
404
405 def _handle_err(self):
406 # We don't want to let this go silently, so eventually we should log.
407 raise zmq.ZMQError()
408
409 def _queue_reply(self, msg):
410 self.msg_queue.put(msg)
411 self.add_io_state(POLLOUT)
343 412
344 413
345 414 #-----------------------------------------------------------------------------
346 415 # Main kernel manager class
347 416 #-----------------------------------------------------------------------------
348 417
349
350 418 class KernelManager(HasTraits):
351 419 """ Manages a kernel for a frontend.
352 420
@@ -380,6 +448,7 b' class KernelManager(HasTraits):'
380 448
381 449 def __init__(self, xreq_address=None, sub_address=None, rep_address=None,
382 450 context=None, session=None):
451 super(KernelManager, self).__init__()
383 452 self._xreq_address = (LOCALHOST, 0) if xreq_address is None else xreq_address
384 453 self._sub_address = (LOCALHOST, 0) if sub_address is None else sub_address
385 454 self._rep_address = (LOCALHOST, 0) if rep_address is None else rep_address
@@ -430,21 +499,18 b' class KernelManager(HasTraits):'
430 499 If random ports (port=0) are being used, this method must be called
431 500 before the channels are created.
432 501 """
433 xreq, sub = self.xreq_address, self.sub_address
434 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST:
502 xreq, sub, rep = self.xreq_address, self.sub_address, self.rep_address
503 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or rep[0] != LOCALHOST:
435 504 raise RuntimeError("Can only launch a kernel on localhost."
436 505 "Make sure that the '*_address' attributes are "
437 506 "configured properly.")
438 507
439 kernel, xrep, pub = launch_kernel(xrep_port=xreq[1], pub_port=sub[1])
508 kernel, xrep, pub, req = launch_kernel(
509 xrep_port=xreq[1], pub_port=sub[1], req_port=rep[1])
440 510 self._kernel = kernel
441 print xrep, pub
442 511 self._xreq_address = (LOCALHOST, xrep)
443 512 self._sub_address = (LOCALHOST, pub)
444 # The rep channel is not fully working yet, but its base class makes
445 # sure the port is not 0. We set to -1 for now until the rep channel
446 # is fully working.
447 self._rep_address = (LOCALHOST, -1)
513 self._rep_address = (LOCALHOST, req)
448 514
449 515 @property
450 516 def has_kernel(self):
General Comments 0
You need to be logged in to leave comments. Login now