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