##// 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()
@@ -1,74 +1,74 b''
1 1 # System library imports
2 2 from pygments.token import Token, is_token_subtype
3 3
4 4
5 5 class CompletionLexer(object):
6 6 """ Uses Pygments and some auxillary information to lex code snippets for
7 7 symbol contexts.
8 8 """
9 9
10 10 # Maps Lexer names to a list of possible name separators
11 11 separator_map = { 'C' : [ '.', '->' ],
12 12 'C++' : [ '.', '->', '::' ],
13 13 'Python' : [ '.' ] }
14 14
15 15 def __init__(self, lexer):
16 16 """ Create a CompletionLexer using the specified Pygments lexer.
17 17 """
18 18 self.lexer = lexer
19 19
20 20 def get_context(self, string):
21 21 """ Assuming the cursor is at the end of the specified string, get the
22 22 context (a list of names) for the symbol at cursor position.
23 23 """
24 24 context = []
25 25 reversed_tokens = list(self._lexer.get_tokens(string))
26 26 reversed_tokens.reverse()
27 27
28 28 # Pygments often tacks on a newline when none is specified in the input.
29 29 # Remove this newline.
30 30 if reversed_tokens and reversed_tokens[0][1].endswith('\n') and \
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):
38 38
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
53 53 # potentially several tokens.
54 54 elif token is Token.Operator or token is Token.Punctuation:
55 55 current_op = text + current_op
56 56
57 57 # Break on anything that is not a Operator, Punctuation, or Name.
58 58 else:
59 59 break
60 60
61 61 return context
62 62
63 63 def get_lexer(self, lexer):
64 64 return self._lexer
65 65
66 66 def set_lexer(self, lexer, name_separators=None):
67 67 self._lexer = lexer
68 68 if name_separators is None:
69 69 self._name_separators = self.separator_map.get(lexer.name, ['.'])
70 70 else:
71 71 self._name_separators = list(name_separators)
72 72
73 73 lexer = property(get_lexer, set_lexer)
74 74
@@ -1,124 +1,124 b''
1 1 # System library imports
2 2 from PyQt4 import QtCore, QtGui
3 3
4 4
5 5 class CompletionWidget(QtGui.QListWidget):
6 6 """ A widget for GUI tab completion.
7 7 """
8 8
9 9 #--------------------------------------------------------------------------
10 10 # 'QWidget' interface
11 11 #--------------------------------------------------------------------------
12 12
13 13 def __init__(self, parent):
14 14 """ Create a completion widget that is attached to the specified Qt
15 15 text edit widget.
16 16 """
17 17 assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
18 18 QtGui.QListWidget.__init__(self, parent)
19 19
20 20 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
21 21 self.setAttribute(QtCore.Qt.WA_StaticContents)
22 22
23 23 # Ensure that parent keeps focus when widget is displayed.
24 24 self.setFocusProxy(parent)
25 25
26 26 self.setFrameShadow(QtGui.QFrame.Plain)
27 27 self.setFrameShape(QtGui.QFrame.StyledPanel)
28 28
29 29 self.itemActivated.connect(self._complete_current)
30 30
31 31 def hideEvent(self, event):
32 32 """ Reimplemented to disconnect the cursor movement handler.
33 33 """
34 34 QtGui.QListWidget.hideEvent(self, event)
35 35 try:
36 36 self.parent().cursorPositionChanged.disconnect(self._update_current)
37 37 except TypeError:
38 38 pass
39 39
40 40 def keyPressEvent(self, event):
41 41 """ Reimplemented to update the list.
42 42 """
43 43 key, text = event.key(), event.text()
44 44
45 45 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
46 46 QtCore.Qt.Key_Tab):
47 47 self._complete_current()
48 48 event.accept()
49 49
50 50 elif key == QtCore.Qt.Key_Escape:
51 51 self.hide()
52 52 event.accept()
53 53
54 54 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
55 55 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
56 56 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
57 57 QtGui.QListWidget.keyPressEvent(self, event)
58 58 event.accept()
59 59
60 60 else:
61 61 event.ignore()
62 62
63 63 def showEvent(self, event):
64 64 """ Reimplemented to connect the cursor movement handler.
65 65 """
66 66 QtGui.QListWidget.showEvent(self, event)
67 67 self.parent().cursorPositionChanged.connect(self._update_current)
68 68
69 69 #--------------------------------------------------------------------------
70 70 # 'CompletionWidget' interface
71 71 #--------------------------------------------------------------------------
72 72
73 73 def show_items(self, cursor, items):
74 74 """ Shows the completion widget with 'items' at the position specified
75 75 by 'cursor'.
76 76 """
77 77 text_edit = self.parent()
78 78 point = text_edit.cursorRect(cursor).bottomRight()
79 79 point = text_edit.mapToGlobal(point)
80 80 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
81 81 if screen_rect.size().height() - point.y() - self.height() < 0:
82 82 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
83 83 point.setY(point.y() - self.height())
84 84 self.move(point)
85 85
86 86 self._start_position = cursor.position()
87 87 self.clear()
88 88 self.addItems(items)
89 89 self.setCurrentRow(0)
90 90 self.show()
91 91
92 92 #--------------------------------------------------------------------------
93 93 # Protected interface
94 94 #--------------------------------------------------------------------------
95 95
96 96 def _complete_current(self):
97 97 """ Perform the completion with the currently selected item.
98 98 """
99 99 self._current_text_cursor().insertText(self.currentItem().text())
100 100 self.hide()
101 101
102 102 def _current_text_cursor(self):
103 103 """ Returns a cursor with text between the start position and the
104 104 current position selected.
105 105 """
106 106 cursor = self.parent().textCursor()
107 107 if cursor.position() >= self._start_position:
108 108 cursor.setPosition(self._start_position,
109 109 QtGui.QTextCursor.KeepAnchor)
110 110 return cursor
111 111
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))
119 119 if items:
120 120 self.setCurrentItem(items[0])
121 121 else:
122 122 self.hide()
123 123 else:
124 124 self.hide()
@@ -1,823 +1,972 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
100 15 handling ANSI escape sequences.
101 16 """
102 17
103 18 # Whether to process ANSI escape codes.
104 19 ansi_codes = True
105 20
106 21 # The maximum number of lines of text before truncation.
107 22 buffer_size = 500
108 23
109 24 # Whether to use a CompletionWidget or plain text output for tab completion.
110 25 gui_completion = True
111 26
112 27 # Whether to override ShortcutEvents for the keybindings defined by this
113 28 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
114 29 # priority (when it has focus) over, e.g., window-level menu shortcuts.
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,
122 36 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
123 37 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
124 38 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
125 39 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
126 40 _shortcuts = set(_ctrl_down_remap.keys() +
127 41 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
128 42
129 43 #---------------------------------------------------------------------------
130 44 # 'QObject' interface
131 45 #---------------------------------------------------------------------------
132 46
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()
147 66
148 67 # Define a custom context menu.
149 68 self._context_menu = QtGui.QMenu(self)
150 69
151 70 copy_action = QtGui.QAction('Copy', self)
152 71 copy_action.triggered.connect(self.copy)
153 72 self.copyAvailable.connect(copy_action.setEnabled)
154 73 self._context_menu.addAction(copy_action)
155 74
156 75 self._paste_action = QtGui.QAction('Paste', self)
157 76 self._paste_action.triggered.connect(self.paste)
158 77 self._context_menu.addAction(self._paste_action)
159 78 self._context_menu.addSeparator()
160 79
161 80 select_all_action = QtGui.QAction('Select All', self)
162 81 select_all_action.triggered.connect(self.selectAll)
163 82 self._context_menu.addAction(select_all_action)
164 83
165 84 def event(self, event):
166 85 """ Reimplemented to override shortcuts, if necessary.
167 86 """
168 87 # On Mac OS, it is always unnecessary to override shortcuts, hence the
169 88 # check below. Users should just use the Control key instead of the
170 89 # Command key.
171 90 if self.override_shortcuts and \
172 91 sys.platform != 'darwin' and \
173 92 event.type() == QtCore.QEvent.ShortcutOverride and \
174 93 self._control_down(event.modifiers()) and \
175 94 event.key() in self._shortcuts:
176 95 event.accept()
177 96 return True
178 97 else:
179 98 return QtGui.QPlainTextEdit.event(self, event)
180 99
181 100 #---------------------------------------------------------------------------
182 101 # 'QWidget' interface
183 102 #---------------------------------------------------------------------------
184 103
185 104 def contextMenuEvent(self, event):
186 105 """ Reimplemented to create a menu without destructive actions like
187 106 'Cut' and 'Delete'.
188 107 """
189 108 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
190 109 self._paste_action.setEnabled(not clipboard_empty)
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 """
197 121 intercepted = False
198 122 cursor = self.textCursor()
199 123 position = cursor.position()
200 124 key = event.key()
201 125 ctrl_down = self._control_down(event.modifiers())
202 126 alt_down = event.modifiers() & QtCore.Qt.AltModifier
203 127 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
204 128
205 129 # Even though we have reimplemented 'paste', the C++ level slot is still
206 130 # called by Qt. So we intercept the key press here.
207 131 if event.matches(QtGui.QKeySequence.Paste):
208 132 self.paste()
209 133 intercepted = True
210 134
211 135 elif ctrl_down:
212 136 if key in self._ctrl_down_remap:
213 137 ctrl_down = False
214 138 key = self._ctrl_down_remap[key]
215 139 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
216 140 QtCore.Qt.NoModifier)
217 141
218 142 elif key == QtCore.Qt.Key_K:
219 143 if self._in_buffer(position):
220 144 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
221 145 QtGui.QTextCursor.KeepAnchor)
222 146 cursor.removeSelectedText()
223 147 intercepted = True
224 148
225 149 elif key == QtCore.Qt.Key_X:
226 150 intercepted = True
227 151
228 152 elif key == QtCore.Qt.Key_Y:
229 153 self.paste()
230 154 intercepted = True
231 155
232 156 elif alt_down:
233 157 if key == QtCore.Qt.Key_B:
234 158 self.setTextCursor(self._get_word_start_cursor(position))
235 159 intercepted = True
236 160
237 161 elif key == QtCore.Qt.Key_F:
238 162 self.setTextCursor(self._get_word_end_cursor(position))
239 163 intercepted = True
240 164
241 165 elif key == QtCore.Qt.Key_Backspace:
242 166 cursor = self._get_word_start_cursor(position)
243 167 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
244 168 cursor.removeSelectedText()
245 169 intercepted = True
246 170
247 171 elif key == QtCore.Qt.Key_D:
248 172 cursor = self._get_word_end_cursor(position)
249 173 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
250 174 cursor.removeSelectedText()
251 175 intercepted = True
252 176
253 177 if self._completion_widget.isVisible():
254 178 self._completion_widget.keyPressEvent(event)
255 179 intercepted = event.isAccepted()
256 180
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
264 191
265 192 elif key == QtCore.Qt.Key_Up:
266 193 if self._reading or not self._up_pressed():
267 194 intercepted = True
268 195 else:
269 196 prompt_line = self._get_prompt_cursor().blockNumber()
270 197 intercepted = cursor.blockNumber() <= prompt_line
271 198
272 199 elif key == QtCore.Qt.Key_Down:
273 200 if self._reading or not self._down_pressed():
274 201 intercepted = True
275 202 else:
276 203 end_line = self._get_end_cursor().blockNumber()
277 204 intercepted = cursor.blockNumber() == end_line
278 205
279 206 elif key == QtCore.Qt.Key_Tab:
280 207 if self._reading:
281 208 intercepted = False
282 209 else:
283 210 intercepted = not self._tab_pressed()
284 211
285 212 elif key == QtCore.Qt.Key_Left:
286 213 intercepted = not self._in_buffer(position - 1)
287 214
288 215 elif key == QtCore.Qt.Key_Home:
289 216 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
290 217 start_pos = cursor.position()
291 218 start_line = cursor.blockNumber()
292 219 if start_line == self._get_prompt_cursor().blockNumber():
293 220 start_pos += len(self._prompt)
294 221 else:
295 222 start_pos += len(self._continuation_prompt)
296 223 if shift_down and self._in_buffer(position):
297 224 self._set_selection(position, start_pos)
298 225 else:
299 226 self._set_position(start_pos)
300 227 intercepted = True
301 228
302 229 elif key == QtCore.Qt.Key_Backspace and not alt_down:
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)
310 238 cursor.removeSelectedText()
311 239
312 240 # Regular backwards deletion
313 241 else:
314 242 anchor = cursor.anchor()
315 243 if anchor == position:
316 244 intercepted = not self._in_buffer(position - 1)
317 245 else:
318 246 intercepted = not self._in_buffer(min(anchor, position))
319 247
320 248 elif key == QtCore.Qt.Key_Delete:
321 249 anchor = cursor.anchor()
322 250 intercepted = not self._in_buffer(min(anchor, position))
323 251
324 252 # Don't move cursor if control is down to allow copy-paste using
325 253 # the keyboard in any part of the buffer.
326 254 if not ctrl_down:
327 255 self._keep_cursor_in_buffer()
328 256
329 257 if not intercepted:
330 258 QtGui.QPlainTextEdit.keyPressEvent(self, event)
331 259
332 260 #--------------------------------------------------------------------------
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
357 299 def clear(self, keep_input=False):
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()
366 307 if keep_input:
367 308 self.input_buffer = input_buffer
368 309
369 310 def paste(self):
370 311 """ Reimplemented to ensure that text is pasted in the editing region.
371 312 """
372 313 self._keep_cursor_in_buffer()
373 314 QtGui.QPlainTextEdit.paste(self)
374 315
375 316 def print_(self, printer):
376 317 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
377 318 slot has the wrong signature.
378 319 """
379 320 QtGui.QPlainTextEdit.print_(self, printer)
380 321
381 322 #---------------------------------------------------------------------------
382 323 # 'ConsoleWidget' public interface
383 324 #---------------------------------------------------------------------------
384 325
385 326 def execute(self, source=None, hidden=False, interactive=False):
386 327 """ Executes source or the input buffer, possibly prompting for more
387 328 input.
388 329
389 330 Parameters:
390 331 -----------
391 332 source : str, optional
392 333
393 334 The source to execute. If not specified, the input buffer will be
394 335 used. If specified and 'hidden' is False, the input buffer will be
395 336 replaced with the source before execution.
396 337
397 338 hidden : bool, optional (default False)
398 339
399 340 If set, no output will be shown and the prompt will not be modified.
400 341 In other words, it will be completely invisible to the user that
401 342 an execution has occurred.
402 343
403 344 interactive : bool, optional (default False)
404 345
405 346 Whether the console is to treat the source as having been manually
406 347 entered by the user. The effect of this parameter depends on the
407 348 subclass implementation.
408 349
409 350 Raises:
410 351 -------
411 352 RuntimeError
412 353 If incomplete input is given and 'hidden' is True. In this case,
413 354 it not possible to prompt for more input.
414 355
415 356 Returns:
416 357 --------
417 358 A boolean indicating whether the source was executed.
418 359 """
419 360 if not hidden:
420 361 if source is not None:
421 362 self.input_buffer = source
422 363
423 364 self.appendPlainText('\n')
424 365 self._executing_input_buffer = self.input_buffer
425 366 self._executing = True
426 367 self._prompt_finished()
427 368
428 369 real_source = self.input_buffer if source is None else source
429 370 complete = self._is_complete(real_source, interactive)
430 371 if complete:
431 372 if not hidden:
432 373 # The maximum block count is only in effect during execution.
433 374 # This ensures that _prompt_pos does not become invalid due to
434 375 # text truncation.
435 376 self.setMaximumBlockCount(self.buffer_size)
436 377 self._execute(real_source, hidden)
437 378 elif hidden:
438 379 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
439 380 else:
440 381 self._show_continuation_prompt()
441 382
442 383 return complete
443 384
444 385 def _get_input_buffer(self):
445 386 """ The text that the user has entered entered at the current prompt.
446 387 """
447 388 # If we're executing, the input buffer may not even exist anymore due to
448 389 # the limit imposed by 'buffer_size'. Therefore, we store it.
449 390 if self._executing:
450 391 return self._executing_input_buffer
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.
460 398 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
461 399
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)
478 421
479 422 def _get_input_buffer_cursor_line(self):
480 423 """ The text in the line of the input buffer in which the user's cursor
481 424 rests. Returns a string if there is such a line; otherwise, None.
482 425 """
483 426 if self._executing:
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:
491 434 return text[len(self._continuation_prompt):]
492 435 else:
493 436 return None
494 437
495 438 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
496 439
497 440 def _get_font(self):
498 441 """ The base font being used by the ConsoleWidget.
499 442 """
500 443 return self.document().defaultFont()
501 444
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
508 454 font = property(_get_font, _set_font)
509 455
510 456 def reset_font(self):
511 457 """ Sets the font to the default fixed-width font for this platform.
512 458 """
513 459 if sys.platform == 'win32':
514 460 name = 'Courier'
515 461 elif sys.platform == 'darwin':
516 462 name = 'Monaco'
517 463 else:
518 464 name = 'Monospace'
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
525 486 #---------------------------------------------------------------------------
526 487
527 488 def _is_complete(self, source, interactive):
528 489 """ Returns whether 'source' can be executed. When triggered by an
529 490 Enter/Return key press, 'interactive' is True; otherwise, it is
530 491 False.
531 492 """
532 493 raise NotImplementedError
533 494
534 495 def _execute(self, source, hidden):
535 496 """ Execute 'source'. If 'hidden', do not show any output.
536 497 """
537 498 raise NotImplementedError
538 499
539 500 def _prompt_started_hook(self):
540 501 """ Called immediately after a new prompt is displayed.
541 502 """
542 503 pass
543 504
544 505 def _prompt_finished_hook(self):
545 506 """ Called immediately after a prompt is finished, i.e. when some input
546 507 will be processed and a new prompt displayed.
547 508 """
548 509 pass
549 510
550 511 def _up_pressed(self):
551 512 """ Called when the up key is pressed. Returns whether to continue
552 513 processing the event.
553 514 """
554 515 return True
555 516
556 517 def _down_pressed(self):
557 518 """ Called when the down key is pressed. Returns whether to continue
558 519 processing the event.
559 520 """
560 521 return True
561 522
562 523 def _tab_pressed(self):
563 524 """ Called when the tab key is pressed. Returns whether to continue
564 525 processing the event.
565 526 """
566 527 return False
567 528
568 529 #--------------------------------------------------------------------------
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
575 557 Control).
576 558 """
577 559 down = bool(modifiers & QtCore.Qt.ControlModifier)
578 560
579 561 # Note: on Mac OS, ControlModifier corresponds to the Command key while
580 562 # MetaModifier corresponds to the Control key.
581 563 if sys.platform == 'darwin':
582 564 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
583 565
584 566 return down
585 567
586 568 def _complete_with_items(self, cursor, items):
587 569 """ Performs completion with 'items' at the specified cursor location.
588 570 """
589 571 if len(items) == 1:
590 572 cursor.setPosition(self.textCursor().position(),
591 573 QtGui.QTextCursor.KeepAnchor)
592 574 cursor.insertText(items[0])
593 575 elif len(items) > 1:
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.
602 660 """
603 661 cursor = self.textCursor()
604 662 cursor.movePosition(QtGui.QTextCursor.End)
605 663 return cursor
606 664
607 665 def _get_prompt_cursor(self):
608 666 """ Convenience method that returns a cursor for the prompt position.
609 667 """
610 668 cursor = self.textCursor()
611 669 cursor.setPosition(self._prompt_pos)
612 670 return cursor
613 671
614 672 def _get_selection_cursor(self, start, end):
615 673 """ Convenience method that returns a cursor with text selected between
616 674 the positions 'start' and 'end'.
617 675 """
618 676 cursor = self.textCursor()
619 677 cursor.setPosition(start)
620 678 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
621 679 return cursor
622 680
623 681 def _get_word_start_cursor(self, position):
624 682 """ Find the start of the word to the left the given position. If a
625 683 sequence of non-word characters precedes the first word, skip over
626 684 them. (This emulates the behavior of bash, emacs, etc.)
627 685 """
628 686 document = self.document()
629 687 position -= 1
630 688 while self._in_buffer(position) and \
631 689 not document.characterAt(position).isLetterOrNumber():
632 690 position -= 1
633 691 while self._in_buffer(position) and \
634 692 document.characterAt(position).isLetterOrNumber():
635 693 position -= 1
636 694 cursor = self.textCursor()
637 695 cursor.setPosition(position + 1)
638 696 return cursor
639 697
640 698 def _get_word_end_cursor(self, position):
641 699 """ Find the end of the word to the right the given position. If a
642 700 sequence of non-word characters precedes the first word, skip over
643 701 them. (This emulates the behavior of bash, emacs, etc.)
644 702 """
645 703 document = self.document()
646 704 end = self._get_end_cursor().position()
647 705 while position < end and \
648 706 not document.characterAt(position).isLetterOrNumber():
649 707 position += 1
650 708 while position < end and \
651 709 document.characterAt(position).isLetterOrNumber():
652 710 position += 1
653 711 cursor = self.textCursor()
654 712 cursor.setPosition(position)
655 713 return cursor
656 714
657 715 def _prompt_started(self):
658 716 """ Called immediately after a new prompt is displayed.
659 717 """
660 718 # Temporarily disable the maximum block count to permit undo/redo and
661 719 # to ensure that the prompt position does not change due to truncation.
662 720 self.setMaximumBlockCount(0)
663 721 self.setUndoRedoEnabled(True)
664 722
665 723 self.setReadOnly(False)
666 724 self.moveCursor(QtGui.QTextCursor.End)
667 725 self.centerCursor()
668 726
669 727 self._executing = False
670 728 self._prompt_started_hook()
671 729
672 730 def _prompt_finished(self):
673 731 """ Called immediately after a prompt is finished, i.e. when some input
674 732 will be processed and a new prompt displayed.
675 733 """
676 734 self.setUndoRedoEnabled(False)
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 """
683 805 cursor = self.textCursor()
684 806 cursor.setPosition(position)
685 807 self.setTextCursor(cursor)
686 808
687 809 def _set_selection(self, start, end):
688 810 """ Convenience method to set the current selected text.
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.
722 871 """
723 872 return position >= self._prompt_pos
724 873
725 874 def _keep_cursor_in_buffer(self):
726 875 """ Ensures that the cursor is inside the editing region. Returns
727 876 whether the cursor was moved.
728 877 """
729 878 cursor = self.textCursor()
730 879 if cursor.position() < self._prompt_pos:
731 880 cursor.movePosition(QtGui.QTextCursor.End)
732 881 self.setTextCursor(cursor)
733 882 return True
734 883 else:
735 884 return False
736 885
737 886
738 887 class HistoryConsoleWidget(ConsoleWidget):
739 888 """ A ConsoleWidget that keeps a history of the commands that have been
740 889 executed.
741 890 """
742 891
743 892 #---------------------------------------------------------------------------
744 893 # 'QObject' interface
745 894 #---------------------------------------------------------------------------
746 895
747 896 def __init__(self, parent=None):
748 897 super(HistoryConsoleWidget, self).__init__(parent)
749 898
750 899 self._history = []
751 900 self._history_index = 0
752 901
753 902 #---------------------------------------------------------------------------
754 903 # 'ConsoleWidget' public interface
755 904 #---------------------------------------------------------------------------
756 905
757 906 def execute(self, source=None, hidden=False, interactive=False):
758 907 """ Reimplemented to the store history.
759 908 """
760 909 if not hidden:
761 910 history = self.input_buffer if source is None else source
762 911
763 912 executed = super(HistoryConsoleWidget, self).execute(
764 913 source, hidden, interactive)
765 914
766 915 if executed and not hidden:
767 916 self._history.append(history.rstrip())
768 917 self._history_index = len(self._history)
769 918
770 919 return executed
771 920
772 921 #---------------------------------------------------------------------------
773 922 # 'ConsoleWidget' abstract interface
774 923 #---------------------------------------------------------------------------
775 924
776 925 def _up_pressed(self):
777 926 """ Called when the up key is pressed. Returns whether to continue
778 927 processing the event.
779 928 """
780 929 prompt_cursor = self._get_prompt_cursor()
781 930 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
782 931 self.history_previous()
783 932
784 933 # Go to the first line of prompt for seemless history scrolling.
785 934 cursor = self._get_prompt_cursor()
786 935 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
787 936 self.setTextCursor(cursor)
788 937
789 938 return False
790 939 return True
791 940
792 941 def _down_pressed(self):
793 942 """ Called when the down key is pressed. Returns whether to continue
794 943 processing the event.
795 944 """
796 945 end_cursor = self._get_end_cursor()
797 946 if self.textCursor().blockNumber() == end_cursor.blockNumber():
798 947 self.history_next()
799 948 return False
800 949 return True
801 950
802 951 #---------------------------------------------------------------------------
803 952 # 'HistoryConsoleWidget' interface
804 953 #---------------------------------------------------------------------------
805 954
806 955 def history_previous(self):
807 956 """ If possible, set the input buffer to the previous item in the
808 957 history.
809 958 """
810 959 if self._history_index > 0:
811 960 self._history_index -= 1
812 961 self.input_buffer = self._history[self._history_index]
813 962
814 963 def history_next(self):
815 964 """ Set the input buffer to the next item in the history, or a blank
816 965 line if there is no subsequent item.
817 966 """
818 967 if self._history_index < len(self._history):
819 968 self._history_index += 1
820 969 if self._history_index < len(self._history):
821 970 self.input_buffer = self._history[self._history_index]
822 971 else:
823 972 self.input_buffer = ''
@@ -1,330 +1,382 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
6 7 from PyQt4 import QtCore, QtGui
7 8 import zmq
8 9
9 10 # Local imports
10 11 from IPython.core.inputsplitter import InputSplitter
11 12 from call_tip_widget import CallTipWidget
12 13 from completion_lexer import CompletionLexer
13 14 from console_widget import HistoryConsoleWidget
14 15 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
27 28
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)
45 61
46 62
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
54 70 #---------------------------------------------------------------------------
55 71 # 'QObject' interface
56 72 #---------------------------------------------------------------------------
57 73
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())
68 80 self._hidden = True
69 81 self._highlighter = FrontendHighlighter(self)
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 #---------------------------------------------------------------------------
76 92 # 'QWidget' interface
77 93 #---------------------------------------------------------------------------
78 94
79 95 def focusOutEvent(self, event):
80 96 """ Reimplemented to hide calltips.
81 97 """
82 98 self._call_tip_widget.hide()
83 99 super(FrontendWidget, self).focusOutEvent(event)
84 100
85 101 def keyPressEvent(self, event):
86 102 """ Reimplemented to allow calltips to process events and to send
87 103 signals to the kernel.
88 104 """
89 105 if self._executing and event.key() == QtCore.Qt.Key_C and \
90 106 self._control_down(event.modifiers()):
91 107 self._interrupt_kernel()
92 108 else:
93 109 if self._call_tip_widget.isVisible():
94 110 self._call_tip_widget.keyPressEvent(event)
95 111 super(FrontendWidget, self).keyPressEvent(event)
96 112
97 113 #---------------------------------------------------------------------------
98 114 # 'ConsoleWidget' abstract interface
99 115 #---------------------------------------------------------------------------
100 116
101 117 def _is_complete(self, source, interactive):
102 118 """ Returns whether 'source' can be completely processed and a new
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
110 126
111 127 def _execute(self, source, hidden):
112 128 """ Execute 'source'. If 'hidden', do not show any output.
113 129 """
114 130 self.kernel_manager.xreq_channel.execute(source)
115 131 self._hidden = hidden
116 132
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
135 155 processing the event.
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
145 163 #---------------------------------------------------------------------------
146 164
147 165 def execute_file(self, path, hidden=False):
148 166 """ Attempts to execute file with 'path'. If 'hidden', no output is
149 167 shown.
150 168 """
151 169 self.execute('execfile("%s")' % path, hidden=hidden)
152 170
153 171 def _get_kernel_manager(self):
154 172 """ Returns the current kernel manager.
155 173 """
156 174 return self._kernel_manager
157 175
158 176 def _set_kernel_manager(self, kernel_manager):
159 177 """ Disconnect from the current kernel manager (if any) and set a new
160 178 kernel manager.
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
183 203 if kernel_manager is None:
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
205 227 #---------------------------------------------------------------------------
206 228 # 'FrontendWidget' protected interface
207 229 #---------------------------------------------------------------------------
208 230
209 231 def _call_tip(self):
210 232 """ Shows a call tip, if appropriate, at the current cursor location.
211 233 """
212 234 # Decide if it makes sense to show a call tip
213 235 cursor = self.textCursor()
214 236 cursor.movePosition(QtGui.QTextCursor.Left)
215 237 document = self.document()
216 238 if document.characterAt(cursor.position()).toAscii() != '(':
217 239 return False
218 240 context = self._get_context(cursor)
219 241 if not context:
220 242 return False
221 243
222 244 # Send the metadata request to the kernel
223 245 name = '.'.join(context)
224 246 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
225 247 self._calltip_pos = self.textCursor().position()
226 248 return True
227 249
228 250 def _complete(self):
229 251 """ Performs completion at the current cursor location.
230 252 """
231 253 # Decide if it makes sense to do completion
232 254 context = self._get_context()
233 255 if not context:
234 256 return False
235 257
236 258 # Send the completion request to the kernel
237 259 text = '.'.join(context)
238 260 self._complete_id = self.kernel_manager.xreq_channel.complete(
239 261 text, self.input_buffer_cursor_line, self.input_buffer)
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 """
246 275 if cursor is None:
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):
254 283 """ Attempts to the interrupt the kernel.
255 284 """
256 285 if self.kernel_manager.has_kernel:
257 286 self.kernel_manager.signal_kernel(signal.SIGINT)
258 287 else:
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.
267 314 """
268 315 # Calculate where the cursor should be *after* the change:
269 316 position += added
270 317
271 318 document = self.document()
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
278 334 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
279 335 if handler is not None:
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
295 349 # Make sure that all output from the SUB channel has been processed
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()
312 370 if rep['parent_header']['msg_id'] == self._complete_id and \
313 371 cursor.position() == self._complete_pos:
314 372 text = '.'.join(self._get_context())
315 373 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
316 374 self._complete_with_items(cursor, rep['content']['matches'])
317 375
318 376 def _handle_object_info_reply(self, rep):
319 377 cursor = self.textCursor()
320 378 if rep['parent_header']['msg_id'] == self._calltip_id and \
321 379 cursor.position() == self._calltip_pos:
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
@@ -1,105 +1,164 b''
1 1 # System library imports
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
8 9 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 #---------------------------------------------------------------------------
15 25
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
23 38 #---------------------------------------------------------------------------
24 39
25 40 def _execute(self, source, hidden):
26 41 """ Reimplemented to override magic commands.
27 42 """
28 43 magic_source = source.strip()
29 44 if magic_source.startswith('%'):
30 45 magic_source = magic_source[1:]
31 46 magic, sep, arguments = magic_source.partition(' ')
32 47 if not magic:
33 48 magic = magic_source
34 49
35 50 callback = self._magic_overrides.get(magic)
36 51 if callback:
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
44 59 #---------------------------------------------------------------------------
45 60 # 'FrontendWidget' interface
46 61 #---------------------------------------------------------------------------
47 62
48 63 def execute_file(self, path, hidden=False):
49 64 """ Reimplemented to use the 'run' magic.
50 65 """
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
57 122 def set_magic_override(self, magic, callback):
58 123 """ Overrides an IPython magic command. This magic will be intercepted
59 124 by the frontend rather than passed on to the kernel and 'callback'
60 125 will be called with a single argument: a string of argument(s) for
61 126 the magic. The callback can (optionally) return text to print to the
62 127 console.
63 128 """
64 129 self._magic_overrides[magic] = callback
65 130
66 131 def remove_magic_override(self, magic):
67 132 """ Removes the override for the specified magic, if there is one.
68 133 """
69 134 try:
70 135 del self._magic_overrides[magic]
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')
103 162 widget.resize(640, 480)
104 163 widget.show()
105 164 app.exec_()
@@ -1,183 +1,182 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
8 8
9 9 def get_tokens_unprocessed(self, text, stack=('root',)):
10 10 """ Split ``text`` into (tokentype, text) pairs.
11 11
12 12 Monkeypatched to store the final stack on the object itself.
13 13 """
14 14 pos = 0
15 15 tokendefs = self._tokens
16 16 if hasattr(self, '_saved_state_stack'):
17 17 statestack = list(self._saved_state_stack)
18 18 else:
19 19 statestack = list(stack)
20 20 statetokens = tokendefs[statestack[-1]]
21 21 while 1:
22 22 for rexmatch, action, new_state in statetokens:
23 23 m = rexmatch(text, pos)
24 24 if m:
25 25 if type(action) is _TokenType:
26 26 yield pos, action, m.group()
27 27 else:
28 28 for item in action(self, m):
29 29 yield item
30 30 pos = m.end()
31 31 if new_state is not None:
32 32 # state transition
33 33 if isinstance(new_state, tuple):
34 34 for state in new_state:
35 35 if state == '#pop':
36 36 statestack.pop()
37 37 elif state == '#push':
38 38 statestack.append(statestack[-1])
39 39 else:
40 40 statestack.append(state)
41 41 elif isinstance(new_state, int):
42 42 # pop
43 43 del statestack[new_state:]
44 44 elif new_state == '#push':
45 45 statestack.append(statestack[-1])
46 46 else:
47 47 assert False, "wrong state def: %r" % new_state
48 48 statetokens = tokendefs[statestack[-1]]
49 49 break
50 50 else:
51 51 try:
52 52 if text[pos] == '\n':
53 53 # at EOL, reset state to "root"
54 54 pos += 1
55 55 statestack = ['root']
56 56 statetokens = tokendefs['root']
57 57 yield pos, Text, u'\n'
58 58 continue
59 59 yield pos, Error, text[pos]
60 60 pos += 1
61 61 except IndexError:
62 62 break
63 63 self._saved_state_stack = list(statestack)
64 64
65 65 # Monkeypatch!
66 66 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
67 67
68 68
69 69 class BlockUserData(QtGui.QTextBlockUserData):
70 70 """ Storage for the user data associated with each line.
71 71 """
72 72
73 73 syntax_stack = ('root',)
74 74
75 75 def __init__(self, **kwds):
76 76 for key, value in kwds.iteritems():
77 77 setattr(self, key, value)
78 78 QtGui.QTextBlockUserData.__init__(self)
79 79
80 80 def __repr__(self):
81 81 attrs = ['syntax_stack']
82 82 kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
83 83 for attr in attrs ])
84 84 return 'BlockUserData(%s)' % kwds
85 85
86 86
87 87 class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
88 88 """ Syntax highlighter that uses Pygments for parsing. """
89 89
90 90 def __init__(self, parent, lexer=None):
91 91 super(PygmentsHighlighter, self).__init__(parent)
92 92
93 93 self._lexer = lexer if lexer else PythonLexer()
94 94 self._style = DefaultStyle
95 95 # Caches for formats and brushes.
96 96 self._brushes = {}
97 97 self._formats = {}
98 98
99 99 def highlightBlock(self, qstring):
100 100 """ Highlight a block of text.
101 101 """
102 102 qstring = unicode(qstring)
103 103 prev_data = self.previous_block_data()
104 104
105 105 if prev_data is not None:
106 106 self._lexer._saved_state_stack = prev_data.syntax_stack
107 107 elif hasattr(self._lexer, '_saved_state_stack'):
108 108 del self._lexer._saved_state_stack
109 109
110 110 index = 0
111 111 # Lex the text using Pygments
112 112 for token, text in self._lexer.get_tokens(qstring):
113 113 l = len(text)
114 114 format = self._get_format(token)
115 115 if format is not None:
116 116 self.setFormat(index, l, format)
117 117 index += l
118 118
119 119 if hasattr(self._lexer, '_saved_state_stack'):
120 120 data = BlockUserData(syntax_stack=self._lexer._saved_state_stack)
121 121 self.currentBlock().setUserData(data)
122 122 # Clean up for the next go-round.
123 123 del self._lexer._saved_state_stack
124 124
125 125 def previous_block_data(self):
126 126 """ Convenience method for returning the previous block's user data.
127 127 """
128 128 return self.currentBlock().previous().userData()
129 129
130 130 def _get_format(self, token):
131 131 """ Returns a QTextCharFormat for token or None.
132 132 """
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()
140 140 if key == 'color':
141 141 result.setForeground(self._get_brush(value))
142 142 elif key == 'bgcolor':
143 143 result.setBackground(self._get_brush(value))
144 144 elif key == 'bold':
145 145 result.setFontWeight(QtGui.QFont.Bold)
146 146 elif key == 'italic':
147 147 result.setFontItalic(True)
148 148 elif key == 'underline':
149 149 result.setUnderlineStyle(
150 150 QtGui.QTextCharFormat.SingleUnderline)
151 151 elif key == 'sans':
152 152 result.setFontStyleHint(QtGui.QFont.SansSerif)
153 153 elif key == 'roman':
154 154 result.setFontStyleHint(QtGui.QFont.Times)
155 155 elif key == 'mono':
156 156 result.setFontStyleHint(QtGui.QFont.TypeWriter)
157 157 elif key == 'border':
158 158 # Borders are normally used for errors. We can't do a border
159 159 # so instead we do a wavy underline
160 160 result.setUnderlineStyle(
161 161 QtGui.QTextCharFormat.WaveUnderline)
162 162 result.setUnderlineColor(self._get_color(value))
163 163 self._formats[token] = result
164 164 return result
165 165
166 166 def _get_brush(self, color):
167 167 """ Returns a brush for the color.
168 168 """
169 169 result = self._brushes.get(color)
170 170 if result is None:
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
183 182
@@ -1,150 +1,185 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 import zmq
7 7
8 8 # IPython imports.
9 9 from IPython.zmq.kernelmanager import KernelManager, SubSocketChannel, \
10 10 XReqSocketChannel, RepSocketChannel
11 11 from util import MetaQObjectHasTraits
12 12
13 13 # When doing multiple inheritance from QtCore.QObject and other classes
14 14 # the calling of the parent __init__'s is a subtle issue:
15 15 # * QtCore.QObject does not call super so you can't use super and put
16 16 # QObject first in the inheritance list.
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
23 26 # Emitted when any message is received.
24 27 message_received = QtCore.pyqtSignal(object)
25 28
26 29 # Emitted when a message of type 'pyout' or 'stdout' is received.
27 30 output_received = QtCore.pyqtSignal(object)
28 31
29 32 # Emitted when a message of type 'pyerr' or 'stderr' is received.
30 33 error_received = QtCore.pyqtSignal(object)
31 34
32 35 #---------------------------------------------------------------------------
33 36 # 'object' interface
34 37 #---------------------------------------------------------------------------
35 38
36 39 def __init__(self, *args, **kw):
37 40 """ Reimplemented to ensure that QtCore.QObject is initialized first.
38 41 """
39 42 QtCore.QObject.__init__(self)
40 43 SubSocketChannel.__init__(self, *args, **kw)
41 44
42 45 #---------------------------------------------------------------------------
43 46 # 'SubSocketChannel' interface
44 47 #---------------------------------------------------------------------------
45 48
46 49 def call_handlers(self, msg):
47 50 """ Reimplemented to emit signals instead of making callbacks.
48 51 """
49 52 # Emit the generic signal.
50 53 self.message_received.emit(msg)
51 54
52 55 # Emit signals for specialized message types.
53 56 msg_type = msg['msg_type']
54 57 if msg_type in ('pyout', 'stdout'):
55 58 self.output_received.emit(msg)
56 59 elif msg_type in ('pyerr', 'stderr'):
57 60 self.error_received.emit(msg)
58 61
59 62 def flush(self):
60 63 """ Reimplemented to ensure that signals are dispatched immediately.
61 64 """
62 65 super(QtSubSocketChannel, self).flush()
63 66 QtCore.QCoreApplication.instance().processEvents()
64 67
65 68
66 69 class QtXReqSocketChannel(XReqSocketChannel, QtCore.QObject):
67 70
68 71 # Emitted when any message is received.
69 72 message_received = QtCore.pyqtSignal(object)
70 73
71 74 # Emitted when a reply has been received for the corresponding request type.
72 75 execute_reply = QtCore.pyqtSignal(object)
73 76 complete_reply = QtCore.pyqtSignal(object)
74 77 object_info_reply = QtCore.pyqtSignal(object)
75 78
76 79 #---------------------------------------------------------------------------
77 80 # 'object' interface
78 81 #---------------------------------------------------------------------------
79 82
80 83 def __init__(self, *args, **kw):
81 84 """ Reimplemented to ensure that QtCore.QObject is initialized first.
82 85 """
83 86 QtCore.QObject.__init__(self)
84 87 XReqSocketChannel.__init__(self, *args, **kw)
85 88
86 89 #---------------------------------------------------------------------------
87 90 # 'XReqSocketChannel' interface
88 91 #---------------------------------------------------------------------------
89 92
90 93 def call_handlers(self, msg):
91 94 """ Reimplemented to emit signals instead of making callbacks.
92 95 """
93 96 # Emit the generic signal.
94 97 self.message_received.emit(msg)
95 98
96 99 # Emit signals for specialized message types.
97 100 msg_type = msg['msg_type']
98 101 signal = getattr(self, msg_type, None)
99 102 if signal:
100 103 signal.emit(msg)
101 104
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 #---------------------------------------------------------------------------
108 117
109 118 def __init__(self, *args, **kw):
110 119 """ Reimplemented to ensure that QtCore.QObject is initialized first.
111 120 """
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 """
118 143
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
129 154 xreq_channel_class = QtXReqSocketChannel
130 155 rep_channel_class = QtRepSocketChannel
131 156
132 157 def __init__(self, *args, **kw):
133 158 QtCore.QObject.__init__(self)
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
140 175 def start_channels(self):
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()
@@ -1,27 +1,22 b''
1 1 """ Defines miscellaneous Qt-related helper classes and functions.
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 HasTraits
9 9
10 10
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.
18 18
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
@@ -1,361 +1,547 b''
1 1 #!/usr/bin/env python
2 2 """A simple interactive kernel that talks to a frontend over 0MQ.
3 3
4 4 Things to do:
5 5
6 6 * Finish implementing `raw_input`.
7 7 * Implement `set_parent` logic. Right before doing exec, the Kernel should
8 8 call set_parent on all the PUB objects with the message about to be executed.
9 9 * Implement random port and security key logic.
10 10 * Implement control messages.
11 11 * Implement event loop and poll version.
12 12 """
13 13
14 #-----------------------------------------------------------------------------
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
23 30
24 31 # Local imports.
25 32 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)
44 142
45 143 def close(self):
46 144 self.pub_socket = None
47 145
48 146 def flush(self):
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):
96 197
97 198 def __init__(self, session, pub_socket):
98 199 self.session = session
99 200 self.pub_socket = pub_socket
100 201 self.parent_header = {}
101 202
102 203 def __call__(self, obj):
103 204 if obj is None:
104 205 return
105 206
106 207 __builtin__._ = obj
107 208 msg = self.session.msg(u'pyout', {u'data':repr(obj)},
108 209 parent=self.parent_header)
109 210 self.pub_socket.send_json(msg)
110 211
111 212 def set_parent(self, parent):
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):
140 219 self.session = session
141 220 self.reply_socket = reply_socket
142 221 self.pub_socket = pub_socket
143 222 self.user_ns = {}
144 223 self.history = []
145 224 self.compiler = CommandCompiler()
146 225 self.completer = KernelCompleter(self.user_ns)
147 226
148 227 # Build dict of handlers for message types
149 228 msg_types = [ 'execute_request', 'complete_request',
150 229 'object_info_request' ]
151 230 self.handlers = {}
152 231 for msg_type in msg_types:
153 232 self.handlers[msg_type] = getattr(self, msg_type)
154 233
155 234 def abort_queue(self):
156 235 while True:
157 236 try:
158 237 ident = self.reply_socket.recv(zmq.NOBLOCK)
159 238 except zmq.ZMQError, e:
160 239 if e.errno == zmq.EAGAIN:
161 240 break
162 241 else:
163 242 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
164 243 msg = self.reply_socket.recv_json()
165 244 print>>sys.__stdout__, "Aborting:"
166 245 print>>sys.__stdout__, Message(msg)
167 246 msg_type = msg['msg_type']
168 247 reply_type = msg_type.split('_')[0] + '_reply'
169 248 reply_msg = self.session.msg(reply_type, {'status' : 'aborted'}, msg)
170 249 print>>sys.__stdout__, Message(reply_msg)
171 250 self.reply_socket.send(ident,zmq.SNDMORE)
172 251 self.reply_socket.send_json(reply_msg)
173 252 # We need to wait a bit for requests to come in. This can probably
174 253 # be set shorter for true asynchronous clients.
175 254 time.sleep(0.1)
176 255
177 256 def execute_request(self, ident, parent):
178 257 try:
179 258 code = parent[u'content'][u'code']
180 259 except:
181 260 print>>sys.__stderr__, "Got bad msg: "
182 261 print>>sys.__stderr__, Message(parent)
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)
189 269 exec comp_code in self.user_ns, self.user_ns
190 270 except:
191 271 result = u'error'
192 272 etype, evalue, tb = sys.exc_info()
193 273 tb = traceback.format_exception(etype, evalue, tb)
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)
201 281 self.pub_socket.send_json(exc_msg)
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)
208 294 self.reply_socket.send_json(reply_msg)
209 295 if reply_msg['content']['status'] == u'error':
210 296 self.abort_queue()
211 297
212 298 def complete_request(self, ident, parent):
213 299 matches = {'matches' : self.complete(parent),
214 300 'status' : 'ok'}
215 301 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
216 302 matches, parent, ident)
217 303 print >> sys.__stdout__, completion_msg
218 304
219 305 def complete(self, msg):
220 306 return self.completer.complete(msg.content.line, msg.content.text)
221 307
222 308 def object_info_request(self, ident, parent):
223 309 context = parent['content']['oname'].split('.')
224 310 object_info = self.object_info(context)
225 311 msg = self.session.send(self.reply_socket, 'object_info_reply',
226 312 object_info, parent, ident)
227 313 print >> sys.__stdout__, msg
228 314
229 315 def object_info(self, context):
230 316 symbol, leftover = self.symbol_from_context(context)
231 317 if symbol is not None and not leftover:
232 318 doc = getattr(symbol, '__doc__', '')
233 319 else:
234 320 doc = ''
235 321 object_info = dict(docstring = doc)
236 322 return object_info
237 323
238 324 def symbol_from_context(self, context):
239 325 if not context:
240 326 return None, context
241 327
242 328 base_symbol_string = context[0]
243 329 symbol = self.user_ns.get(base_symbol_string, None)
244 330 if symbol is None:
245 331 symbol = __builtin__.__dict__.get(base_symbol_string, None)
246 332 if symbol is None:
247 333 return None, context
248 334
249 335 context = context[1:]
250 336 for i, name in enumerate(context):
251 337 new_symbol = getattr(symbol, name, None)
252 338 if new_symbol is None:
253 339 return symbol, context[i:]
254 340 else:
255 341 symbol = new_symbol
256 342
257 343 return symbol, []
258 344
259 345 def start(self):
260 346 while True:
261 347 ident = self.reply_socket.recv()
262 348 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
263 349 msg = self.reply_socket.recv_json()
264 350 omsg = Message(msg)
265 351 print>>sys.__stdout__
266 352 print>>sys.__stdout__, omsg
267 353 handler = self.handlers.get(omsg.msg_type, None)
268 354 if handler is None:
269 355 print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg
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 """
289 418 # Parse command line arguments.
290 419 parser = ArgumentParser()
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')
303 441
304 442 reply_socket = context.socket(zmq.XREP)
305 443 xrep_port = bind_port(reply_socket, namespace.ip, namespace.xrep)
306 444 print >>sys.__stdout__, "XREP Channel on port", xrep_port
307 445
308 446 pub_socket = context.socket(zmq.PUB)
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))
343 510 ports.append(sock)
344 511 for i, sock in enumerate(ports):
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__':
361 547 main()
@@ -1,529 +1,595 b''
1 1 """Classes to manage the interaction with a running kernel.
2 2
3 3 Todo
4 4 ====
5 5
6 6 * Create logger to handle debugging and console messages.
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2008-2010 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # Standard library imports.
21 21 from Queue import Queue, Empty
22 22 from subprocess import Popen
23 23 from threading import Thread
24 24 import time
25 25
26 26 # System library imports.
27 27 import zmq
28 28 from zmq import POLLIN, POLLOUT, POLLERR
29 29 from zmq.eventloop import ioloop
30 30
31 31 # Local imports.
32 32 from IPython.utils.traitlets import HasTraits, Any, Instance, Type
33 33 from kernel import launch_kernel
34 34 from session import Session
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Constants and exceptions
38 38 #-----------------------------------------------------------------------------
39 39
40 40 LOCALHOST = '127.0.0.1'
41 41
42 42 class InvalidPortNumber(Exception):
43 43 pass
44 44
45 45 #-----------------------------------------------------------------------------
46 46 # ZMQ Socket Channel classes
47 47 #-----------------------------------------------------------------------------
48 48
49 49 class ZmqSocketChannel(Thread):
50 50 """The base class for the channels that use ZMQ sockets.
51 51 """
52 52 context = None
53 53 session = None
54 54 socket = None
55 55 ioloop = None
56 56 iostate = None
57 57 _address = None
58 58
59 59 def __init__(self, context, session, address):
60 60 """Create a channel
61 61
62 62 Parameters
63 63 ----------
64 64 context : zmq.Context
65 65 The ZMQ context to use.
66 66 session : session.Session
67 67 The session to use.
68 68 address : tuple
69 69 Standard (ip, port) tuple that the kernel is listening on.
70 70 """
71 71 super(ZmqSocketChannel, self).__init__()
72 72 self.daemon = True
73 73
74 74 self.context = context
75 75 self.session = session
76 76 if address[1] == 0:
77 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):
81 82 """Stop the channel's activity.
82 83
83 84 This calls :method:`Thread.join` and returns when the thread
84 85 terminates. :class:`RuntimeError` will be raised if
85 86 :method:`self.start` is called again.
86 87 """
87 88 self.join()
88 89
89 90 @property
90 91 def address(self):
91 92 """Get the channel's address as an (ip, port) tuple.
92 93
93 94 By the default, the address is (localhost, 0), where 0 means a random
94 95 port.
95 96 """
96 97 return self._address
97 98
98 99 def add_io_state(self, state):
99 100 """Add IO state to the eventloop.
100 101
101 102 Parameters
102 103 ----------
103 104 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
104 105 The IO state flag to set.
105 106
106 107 This is thread safe as it uses the thread safe IOLoop.add_callback.
107 108 """
108 109 def add_io_state_callback():
109 110 if not self.iostate & state:
110 111 self.iostate = self.iostate | state
111 112 self.ioloop.update_handler(self.socket, self.iostate)
112 113 self.ioloop.add_callback(add_io_state_callback)
113 114
114 115 def drop_io_state(self, state):
115 116 """Drop IO state from the eventloop.
116 117
117 118 Parameters
118 119 ----------
119 120 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
120 121 The IO state flag to set.
121 122
122 123 This is thread safe as it uses the thread safe IOLoop.add_callback.
123 124 """
124 125 def drop_io_state_callback():
125 126 if self.iostate & state:
126 127 self.iostate = self.iostate & (~state)
127 128 self.ioloop.update_handler(self.socket, self.iostate)
128 129 self.ioloop.add_callback(drop_io_state_callback)
129 130
130 131
131 132 class XReqSocketChannel(ZmqSocketChannel):
132 133 """The XREQ channel for issues request/replies to the kernel.
133 134 """
134 135
135 136 command_queue = None
136 137
137 138 def __init__(self, context, session, address):
138 139 self.command_queue = Queue()
139 140 super(XReqSocketChannel, self).__init__(context, session, address)
140 141
141 142 def run(self):
142 143 """The thread's main activity. Call start() instead."""
143 144 self.socket = self.context.socket(zmq.XREQ)
144 145 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
145 146 self.socket.connect('tcp://%s:%i' % self.address)
146 147 self.ioloop = ioloop.IOLoop()
147 148 self.iostate = POLLERR|POLLIN
148 149 self.ioloop.add_handler(self.socket, self._handle_events,
149 150 self.iostate)
150 151 self.ioloop.start()
151 152
152 153 def stop(self):
153 154 self.ioloop.stop()
154 155 super(XReqSocketChannel, self).stop()
155 156
156 157 def call_handlers(self, msg):
157 158 """This method is called in the ioloop thread when a message arrives.
158 159
159 160 Subclasses should override this method to handle incoming messages.
160 161 It is important to remember that this method is called in the thread
161 162 so that some logic must be done to ensure that the application leve
162 163 handlers are called in the application thread.
163 164 """
164 165 raise NotImplementedError('call_handlers must be defined in a subclass.')
165 166
166 167 def execute(self, code):
167 168 """Execute code in the kernel.
168 169
169 170 Parameters
170 171 ----------
171 172 code : str
172 173 A string of Python code.
173 174
174 175 Returns
175 176 -------
176 177 The msg_id of the message sent.
177 178 """
178 179 # Create class for content/msg creation. Related to, but possibly
179 180 # not in Session.
180 181 content = dict(code=code)
181 182 msg = self.session.msg('execute_request', content)
182 183 self._queue_request(msg)
183 184 return msg['header']['msg_id']
184 185
185 186 def complete(self, text, line, block=None):
186 187 """Tab complete text, line, block in the kernel's namespace.
187 188
188 189 Parameters
189 190 ----------
190 191 text : str
191 192 The text to complete.
192 193 line : str
193 194 The full line of text that is the surrounding context for the
194 195 text to complete.
195 196 block : str
196 197 The full block of code in which the completion is being requested.
197 198
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)
205 205 self._queue_request(msg)
206 206 return msg['header']['msg_id']
207 207
208 208 def object_info(self, oname):
209 209 """Get metadata information about an object.
210 210
211 211 Parameters
212 212 ----------
213 213 oname : str
214 214 A string specifying the object name.
215 215
216 216 Returns
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)
224 223 return msg['header']['msg_id']
225 224
226 225 def _handle_events(self, socket, events):
227 226 if events & POLLERR:
228 227 self._handle_err()
229 228 if events & POLLOUT:
230 229 self._handle_send()
231 230 if events & POLLIN:
232 231 self._handle_recv()
233 232
234 233 def _handle_recv(self):
235 234 msg = self.socket.recv_json()
236 235 self.call_handlers(msg)
237 236
238 237 def _handle_send(self):
239 238 try:
240 239 msg = self.command_queue.get(False)
241 240 except Empty:
242 241 pass
243 242 else:
244 243 self.socket.send_json(msg)
245 244 if self.command_queue.empty():
246 245 self.drop_io_state(POLLOUT)
247 246
248 247 def _handle_err(self):
249 248 # We don't want to let this go silently, so eventually we should log.
250 249 raise zmq.ZMQError()
251 250
252 251 def _queue_request(self, msg):
253 252 self.command_queue.put(msg)
254 253 self.add_io_state(POLLOUT)
255 254
256 255
257 256 class SubSocketChannel(ZmqSocketChannel):
258 257 """The SUB channel which listens for messages that the kernel publishes.
259 258 """
260 259
261 260 def __init__(self, context, session, address):
262 261 super(SubSocketChannel, self).__init__(context, session, address)
263 262
264 263 def run(self):
265 264 """The thread's main activity. Call start() instead."""
266 265 self.socket = self.context.socket(zmq.SUB)
267 266 self.socket.setsockopt(zmq.SUBSCRIBE,'')
268 267 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
269 268 self.socket.connect('tcp://%s:%i' % self.address)
270 269 self.ioloop = ioloop.IOLoop()
271 270 self.iostate = POLLIN|POLLERR
272 271 self.ioloop.add_handler(self.socket, self._handle_events,
273 272 self.iostate)
274 273 self.ioloop.start()
275 274
276 275 def stop(self):
277 276 self.ioloop.stop()
278 277 super(SubSocketChannel, self).stop()
279 278
280 279 def call_handlers(self, msg):
281 280 """This method is called in the ioloop thread when a message arrives.
282 281
283 282 Subclasses should override this method to handle incoming messages.
284 283 It is important to remember that this method is called in the thread
285 284 so that some logic must be done to ensure that the application leve
286 285 handlers are called in the application thread.
287 286 """
288 287 raise NotImplementedError('call_handlers must be defined in a subclass.')
289 288
290 289 def flush(self, timeout=1.0):
291 290 """Immediately processes all pending messages on the SUB channel.
292 291
293 292 This method is thread safe.
294 293
295 294 Parameters
296 295 ----------
297 296 timeout : float, optional
298 297 The maximum amount of time to spend flushing, in seconds. The
299 298 default is one second.
300 299 """
301 300 # We do the IOLoop callback process twice to ensure that the IOLoop
302 301 # gets to perform at least one full poll.
303 302 stop_time = time.time() + timeout
304 303 for i in xrange(2):
305 304 self._flushed = False
306 305 self.ioloop.add_callback(self._flush)
307 306 while not self._flushed and time.time() < stop_time:
308 307 time.sleep(0.01)
309 308
310 309 def _handle_events(self, socket, events):
311 310 # Turn on and off POLLOUT depending on if we have made a request
312 311 if events & POLLERR:
313 312 self._handle_err()
314 313 if events & POLLIN:
315 314 self._handle_recv()
316 315
317 316 def _handle_err(self):
318 317 # We don't want to let this go silently, so eventually we should log.
319 318 raise zmq.ZMQError()
320 319
321 320 def _handle_recv(self):
322 321 # Get all of the messages we can
323 322 while True:
324 323 try:
325 324 msg = self.socket.recv_json(zmq.NOBLOCK)
326 325 except zmq.ZMQError:
327 326 # Check the errno?
328 327 # Will this tigger POLLERR?
329 328 break
330 329 else:
331 330 self.call_handlers(msg)
332 331
333 332 def _flush(self):
334 333 """Callback for :method:`self.flush`."""
335 334 self._flushed = True
336 335
337 336
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
353 421 The SUB channel is for the frontend to receive messages published by the
354 422 kernel.
355 423
356 424 The REQ channel is for the frontend to make requests of the kernel.
357 425
358 426 The REP channel is for the kernel to request stdin (raw_input) from the
359 427 frontend.
360 428 """
361 429 # The PyZMQ Context to use for communication with the kernel.
362 430 context = Instance(zmq.Context)
363 431
364 432 # The Session to use for communication with the kernel.
365 433 session = Instance(Session)
366 434
367 435 # The classes to use for the various channels.
368 436 xreq_channel_class = Type(XReqSocketChannel)
369 437 sub_channel_class = Type(SubSocketChannel)
370 438 rep_channel_class = Type(RepSocketChannel)
371 439
372 440 # Protected traits.
373 441 _kernel = Instance(Popen)
374 442 _xreq_address = Any
375 443 _sub_address = Any
376 444 _rep_address = Any
377 445 _xreq_channel = Any
378 446 _sub_channel = Any
379 447 _rep_channel = Any
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
386 455 self.context = zmq.Context() if context is None else context
387 456 self.session = Session() if session is None else session
388 457 super(KernelManager, self).__init__()
389 458
390 459 #--------------------------------- -----------------------------------------
391 460 # Channel management methods:
392 461 #--------------------------------------------------------------------------
393 462
394 463 def start_channels(self):
395 464 """Starts the channels for this kernel.
396 465
397 466 This will create the channels if they do not exist and then start
398 467 them. If port numbers of 0 are being used (random ports) then you
399 468 must first call :method:`start_kernel`. If the channels have been
400 469 stopped and you call this, :class:`RuntimeError` will be raised.
401 470 """
402 471 self.xreq_channel.start()
403 472 self.sub_channel.start()
404 473 self.rep_channel.start()
405 474
406 475 def stop_channels(self):
407 476 """Stops the channels for this kernel.
408 477
409 478 This stops the channels by joining their threads. If the channels
410 479 were not started, :class:`RuntimeError` will be raised.
411 480 """
412 481 self.xreq_channel.stop()
413 482 self.sub_channel.stop()
414 483 self.rep_channel.stop()
415 484
416 485 @property
417 486 def channels_running(self):
418 487 """Are all of the channels created and running?"""
419 488 return self.xreq_channel.is_alive() \
420 489 and self.sub_channel.is_alive() \
421 490 and self.rep_channel.is_alive()
422 491
423 492 #--------------------------------------------------------------------------
424 493 # Kernel process management methods:
425 494 #--------------------------------------------------------------------------
426 495
427 496 def start_kernel(self):
428 497 """Starts a kernel process and configures the manager to use it.
429 498
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):
451 517 """Returns whether a kernel process has been specified for the kernel
452 518 manager.
453 519
454 520 A kernel process can be set via 'start_kernel' or 'set_kernel'.
455 521 """
456 522 return self._kernel is not None
457 523
458 524 def kill_kernel(self):
459 525 """ Kill the running kernel. """
460 526 if self._kernel is not None:
461 527 self._kernel.kill()
462 528 self._kernel = None
463 529 else:
464 530 raise RuntimeError("Cannot kill kernel. No kernel is running!")
465 531
466 532 def signal_kernel(self, signum):
467 533 """ Sends a signal to the kernel. """
468 534 if self._kernel is not None:
469 535 self._kernel.send_signal(signum)
470 536 else:
471 537 raise RuntimeError("Cannot signal kernel. No kernel is running!")
472 538
473 539 @property
474 540 def is_alive(self):
475 541 """Is the kernel process still running?"""
476 542 if self._kernel is not None:
477 543 if self._kernel.poll() is None:
478 544 return True
479 545 else:
480 546 return False
481 547 else:
482 548 # We didn't start the kernel with this KernelManager so we don't
483 549 # know if it is running. We should use a heartbeat for this case.
484 550 return True
485 551
486 552 #--------------------------------------------------------------------------
487 553 # Channels used for communication with the kernel:
488 554 #--------------------------------------------------------------------------
489 555
490 556 @property
491 557 def xreq_channel(self):
492 558 """Get the REQ socket channel object to make requests of the kernel."""
493 559 if self._xreq_channel is None:
494 560 self._xreq_channel = self.xreq_channel_class(self.context,
495 561 self.session,
496 562 self.xreq_address)
497 563 return self._xreq_channel
498 564
499 565 @property
500 566 def sub_channel(self):
501 567 """Get the SUB socket channel object."""
502 568 if self._sub_channel is None:
503 569 self._sub_channel = self.sub_channel_class(self.context,
504 570 self.session,
505 571 self.sub_address)
506 572 return self._sub_channel
507 573
508 574 @property
509 575 def rep_channel(self):
510 576 """Get the REP socket channel object to handle stdin (raw_input)."""
511 577 if self._rep_channel is None:
512 578 self._rep_channel = self.rep_channel_class(self.context,
513 579 self.session,
514 580 self.rep_address)
515 581 return self._rep_channel
516 582
517 583 @property
518 584 def xreq_address(self):
519 585 return self._xreq_address
520 586
521 587 @property
522 588 def sub_address(self):
523 589 return self._sub_address
524 590
525 591 @property
526 592 def rep_address(self):
527 593 return self._rep_address
528 594
529 595
General Comments 0
You need to be logged in to leave comments. Login now