##// END OF EJS Templates
* Moved AnsiCodeProcessor to separate file, refactored its API, and added unit tests....
epatters -
Show More
@@ -0,0 +1,131 b''
1 # Standard library imports
2 import re
3
4 # System library imports
5 from PyQt4 import QtCore, QtGui
6
7
8 class AnsiCodeProcessor(object):
9 """ Translates ANSI escape codes into readable attributes.
10 """
11
12 # Protected class variables.
13 _ansi_commands = 'ABCDEFGHJKSTfmnsu'
14 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands)
15
16 def __init__(self):
17 self.reset()
18
19 def reset(self):
20 """ Reset attributs to their default values.
21 """
22 self.intensity = 0
23 self.italic = False
24 self.bold = False
25 self.underline = False
26 self.foreground_color = None
27 self.background_color = None
28
29 def split_string(self, string):
30 """ Yields substrings for which the same escape code applies.
31 """
32 start = 0
33
34 for match in self._ansi_pattern.finditer(string):
35 substring = string[start:match.start()]
36 if substring:
37 yield substring
38 start = match.end()
39
40 params = map(int, match.group(1).split(';'))
41 self.set_csi_code(match.group(2), params)
42
43 substring = string[start:]
44 if substring:
45 yield substring
46
47 def set_csi_code(self, command, params=[]):
48 """ Set attributes based on CSI (Control Sequence Introducer) code.
49
50 Parameters
51 ----------
52 command : str
53 The code identifier, i.e. the final character in the sequence.
54
55 params : sequence of integers, optional
56 The parameter codes for the command.
57 """
58 if command == 'm': # SGR - Select Graphic Rendition
59 for code in params:
60 self.set_sgr_code(code)
61
62 def set_sgr_code(self, code):
63 """ Set attributes based on SGR (Select Graphic Rendition) code.
64 """
65 if code == 0:
66 self.reset()
67 elif code == 1:
68 self.intensity = 1
69 self.bold = True
70 elif code == 2:
71 self.intensity = 0
72 elif code == 3:
73 self.italic = True
74 elif code == 4:
75 self.underline = True
76 elif code == 22:
77 self.intensity = 0
78 self.bold = False
79 elif code == 23:
80 self.italic = False
81 elif code == 24:
82 self.underline = False
83 elif code >= 30 and code <= 37:
84 self.foreground_color = code - 30
85 elif code == 39:
86 self.foreground_color = None
87 elif code >= 40 and code <= 47:
88 self.background_color = code - 40
89 elif code == 49:
90 self.background_color = None
91
92
93 class QtAnsiCodeProcessor(AnsiCodeProcessor):
94 """ Translates ANSI escape codes into QTextCharFormats.
95 """
96
97 # A map from color codes to RGB colors.
98 ansi_colors = ( # Normal, Bright/Light
99 ('#000000', '#7f7f7f'), # 0: black
100 ('#cd0000', '#ff0000'), # 1: red
101 ('#00cd00', '#00ff00'), # 2: green
102 ('#cdcd00', '#ffff00'), # 3: yellow
103 ('#0000ee', '#0000ff'), # 4: blue
104 ('#cd00cd', '#ff00ff'), # 5: magenta
105 ('#00cdcd', '#00ffff'), # 6: cyan
106 ('#e5e5e5', '#ffffff')) # 7: white
107
108 def get_format(self):
109 """ Returns a QTextCharFormat that encodes the current style attributes.
110 """
111 format = QtGui.QTextCharFormat()
112
113 # Set foreground color
114 if self.foreground_color is not None:
115 color = self.ansi_colors[self.foreground_color][self.intensity]
116 format.setForeground(QtGui.QColor(color))
117
118 # Set background color
119 if self.background_color is not None:
120 color = self.ansi_colors[self.background_color][self.intensity]
121 format.setBackground(QtGui.QColor(color))
122
123 # Set font weight/style options
124 if self.bold:
125 format.setFontWeight(QtGui.QFont.Bold)
126 else:
127 format.setFontWeight(QtGui.QFont.Normal)
128 format.setFontItalic(self.italic)
129 format.setFontUnderline(self.underline)
130
131 return format
@@ -0,0 +1,32 b''
1 # Standard library imports
2 import unittest
3
4 # Local imports
5 from IPython.frontend.qt.console.ansi_code_processor import AnsiCodeProcessor
6
7
8 class TestAnsiCodeProcessor(unittest.TestCase):
9
10 def setUp(self):
11 self.processor = AnsiCodeProcessor()
12
13 def testColors(self):
14 string = "first\x1b[34mblue\x1b[0mlast"
15 i = -1
16 for i, substring in enumerate(self.processor.split_string(string)):
17 if i == 0:
18 self.assertEquals(substring, 'first')
19 self.assertEquals(self.processor.foreground_color, None)
20 elif i == 1:
21 self.assertEquals(substring, 'blue')
22 self.assertEquals(self.processor.foreground_color, 4)
23 elif i == 2:
24 self.assertEquals(substring, 'last')
25 self.assertEquals(self.processor.foreground_color, None)
26 else:
27 self.fail("Too many substrings.")
28 self.assertEquals(i, 2, "Too few substrings.")
29
30
31 if __name__ == '__main__':
32 unittest.main()
@@ -1,973 +1,887 b''
1 # Standard library imports
1 # Standard library imports
2 import re
3 import sys
2 import sys
4
3
5 # System library imports
4 # System library imports
6 from PyQt4 import QtCore, QtGui
5 from PyQt4 import QtCore, QtGui
7
6
8 # Local imports
7 # Local imports
8 from ansi_code_processor import QtAnsiCodeProcessor
9 from completion_widget import CompletionWidget
9 from completion_widget import CompletionWidget
10
10
11
11
12 class AnsiCodeProcessor(object):
13 """ Translates ANSI escape codes into readable attributes.
14 """
15
16 def __init__(self):
17 self.ansi_colors = ( # Normal, Bright/Light
18 ('#000000', '#7f7f7f'), # 0: black
19 ('#cd0000', '#ff0000'), # 1: red
20 ('#00cd00', '#00ff00'), # 2: green
21 ('#cdcd00', '#ffff00'), # 3: yellow
22 ('#0000ee', '#0000ff'), # 4: blue
23 ('#cd00cd', '#ff00ff'), # 5: magenta
24 ('#00cdcd', '#00ffff'), # 6: cyan
25 ('#e5e5e5', '#ffffff')) # 7: white
26 self.reset()
27
28 def set_code(self, code):
29 """ Set attributes based on code.
30 """
31 if code == 0:
32 self.reset()
33 elif code == 1:
34 self.intensity = 1
35 self.bold = True
36 elif code == 3:
37 self.italic = True
38 elif code == 4:
39 self.underline = True
40 elif code == 22:
41 self.intensity = 0
42 self.bold = False
43 elif code == 23:
44 self.italic = False
45 elif code == 24:
46 self.underline = False
47 elif code >= 30 and code <= 37:
48 self.foreground_color = code - 30
49 elif code == 39:
50 self.foreground_color = None
51 elif code >= 40 and code <= 47:
52 self.background_color = code - 40
53 elif code == 49:
54 self.background_color = None
55
56 def reset(self):
57 """ Reset attributs to their default values.
58 """
59 self.intensity = 0
60 self.italic = False
61 self.bold = False
62 self.underline = False
63 self.foreground_color = None
64 self.background_color = None
65
66
67 class QtAnsiCodeProcessor(AnsiCodeProcessor):
68 """ Translates ANSI escape codes into QTextCharFormats.
69 """
70
71 def get_format(self):
72 """ Returns a QTextCharFormat that encodes the current style attributes.
73 """
74 format = QtGui.QTextCharFormat()
75
76 # Set foreground color
77 if self.foreground_color is not None:
78 color = self.ansi_colors[self.foreground_color][self.intensity]
79 format.setForeground(QtGui.QColor(color))
80
81 # Set background color
82 if self.background_color is not None:
83 color = self.ansi_colors[self.background_color][self.intensity]
84 format.setBackground(QtGui.QColor(color))
85
86 # Set font weight/style options
87 if self.bold:
88 format.setFontWeight(QtGui.QFont.Bold)
89 else:
90 format.setFontWeight(QtGui.QFont.Normal)
91 format.setFontItalic(self.italic)
92 format.setFontUnderline(self.underline)
93
94 return format
95
96
97 class ConsoleWidget(QtGui.QPlainTextEdit):
12 class ConsoleWidget(QtGui.QPlainTextEdit):
98 """ Base class for console-type widgets. This class is mainly concerned with
13 """ 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
14 dealing with the prompt, keeping the cursor inside the editing line, and
100 handling ANSI escape sequences.
15 handling ANSI escape sequences.
101 """
16 """
102
17
103 # Whether to process ANSI escape codes.
18 # Whether to process ANSI escape codes.
104 ansi_codes = True
19 ansi_codes = True
105
20
106 # The maximum number of lines of text before truncation.
21 # The maximum number of lines of text before truncation.
107 buffer_size = 500
22 buffer_size = 500
108
23
109 # Whether to use a CompletionWidget or plain text output for tab completion.
24 # Whether to use a CompletionWidget or plain text output for tab completion.
110 gui_completion = True
25 gui_completion = True
111
26
112 # Whether to override ShortcutEvents for the keybindings defined by this
27 # 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
28 # 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.
29 # priority (when it has focus) over, e.g., window-level menu shortcuts.
115 override_shortcuts = False
30 override_shortcuts = False
116
31
32 # The number of spaces to show for a tab character.
33 tab_width = 4
34
117 # Protected class variables.
35 # Protected class variables.
118 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
119 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
36 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
120 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
37 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
121 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
38 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
122 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
39 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
123 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
40 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
124 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
41 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
125 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
42 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
126 _shortcuts = set(_ctrl_down_remap.keys() +
43 _shortcuts = set(_ctrl_down_remap.keys() +
127 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
44 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
128
45
129 #---------------------------------------------------------------------------
46 #---------------------------------------------------------------------------
130 # 'QObject' interface
47 # 'QObject' interface
131 #---------------------------------------------------------------------------
48 #---------------------------------------------------------------------------
132
49
133 def __init__(self, parent=None):
50 def __init__(self, parent=None):
134 QtGui.QPlainTextEdit.__init__(self, parent)
51 QtGui.QPlainTextEdit.__init__(self, parent)
135
52
136 # Initialize protected variables. Some variables contain useful state
53 # Initialize protected variables. Some variables contain useful state
137 # information for subclasses; they should be considered read-only.
54 # information for subclasses; they should be considered read-only.
138 self._ansi_processor = QtAnsiCodeProcessor()
55 self._ansi_processor = QtAnsiCodeProcessor()
139 self._completion_widget = CompletionWidget(self)
56 self._completion_widget = CompletionWidget(self)
140 self._continuation_prompt = '> '
57 self._continuation_prompt = '> '
141 self._continuation_prompt_html = None
58 self._continuation_prompt_html = None
142 self._executing = False
59 self._executing = False
143 self._prompt = ''
60 self._prompt = ''
144 self._prompt_html = None
61 self._prompt_html = None
145 self._prompt_pos = 0
62 self._prompt_pos = 0
146 self._reading = False
63 self._reading = False
147 self._reading_callback = None
64 self._reading_callback = None
148
65
149 # Set a monospaced font.
66 # Set a monospaced font.
150 self.reset_font()
67 self.reset_font()
151
68
152 # Define a custom context menu.
69 # Define a custom context menu.
153 self._context_menu = QtGui.QMenu(self)
70 self._context_menu = QtGui.QMenu(self)
154
71
155 copy_action = QtGui.QAction('Copy', self)
72 copy_action = QtGui.QAction('Copy', self)
156 copy_action.triggered.connect(self.copy)
73 copy_action.triggered.connect(self.copy)
157 self.copyAvailable.connect(copy_action.setEnabled)
74 self.copyAvailable.connect(copy_action.setEnabled)
158 self._context_menu.addAction(copy_action)
75 self._context_menu.addAction(copy_action)
159
76
160 self._paste_action = QtGui.QAction('Paste', self)
77 self._paste_action = QtGui.QAction('Paste', self)
161 self._paste_action.triggered.connect(self.paste)
78 self._paste_action.triggered.connect(self.paste)
162 self._context_menu.addAction(self._paste_action)
79 self._context_menu.addAction(self._paste_action)
163 self._context_menu.addSeparator()
80 self._context_menu.addSeparator()
164
81
165 select_all_action = QtGui.QAction('Select All', self)
82 select_all_action = QtGui.QAction('Select All', self)
166 select_all_action.triggered.connect(self.selectAll)
83 select_all_action.triggered.connect(self.selectAll)
167 self._context_menu.addAction(select_all_action)
84 self._context_menu.addAction(select_all_action)
168
85
169 def event(self, event):
86 def event(self, event):
170 """ Reimplemented to override shortcuts, if necessary.
87 """ Reimplemented to override shortcuts, if necessary.
171 """
88 """
172 # On Mac OS, it is always unnecessary to override shortcuts, hence the
89 # On Mac OS, it is always unnecessary to override shortcuts, hence the
173 # check below. Users should just use the Control key instead of the
90 # check below. Users should just use the Control key instead of the
174 # Command key.
91 # Command key.
175 if self.override_shortcuts and \
92 if self.override_shortcuts and \
176 sys.platform != 'darwin' and \
93 sys.platform != 'darwin' and \
177 event.type() == QtCore.QEvent.ShortcutOverride and \
94 event.type() == QtCore.QEvent.ShortcutOverride and \
178 self._control_down(event.modifiers()) and \
95 self._control_down(event.modifiers()) and \
179 event.key() in self._shortcuts:
96 event.key() in self._shortcuts:
180 event.accept()
97 event.accept()
181 return True
98 return True
182 else:
99 else:
183 return QtGui.QPlainTextEdit.event(self, event)
100 return QtGui.QPlainTextEdit.event(self, event)
184
101
185 #---------------------------------------------------------------------------
102 #---------------------------------------------------------------------------
186 # 'QWidget' interface
103 # 'QWidget' interface
187 #---------------------------------------------------------------------------
104 #---------------------------------------------------------------------------
188
105
189 def contextMenuEvent(self, event):
106 def contextMenuEvent(self, event):
190 """ Reimplemented to create a menu without destructive actions like
107 """ Reimplemented to create a menu without destructive actions like
191 'Cut' and 'Delete'.
108 'Cut' and 'Delete'.
192 """
109 """
193 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
110 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
194 self._paste_action.setEnabled(not clipboard_empty)
111 self._paste_action.setEnabled(not clipboard_empty)
195
112
196 self._context_menu.exec_(event.globalPos())
113 self._context_menu.exec_(event.globalPos())
197
114
198 def dragMoveEvent(self, event):
115 def dragMoveEvent(self, event):
199 """ Reimplemented to disable dropping text.
116 """ Reimplemented to disable dropping text.
200 """
117 """
201 event.ignore()
118 event.ignore()
202
119
203 def keyPressEvent(self, event):
120 def keyPressEvent(self, event):
204 """ Reimplemented to create a console-like interface.
121 """ Reimplemented to create a console-like interface.
205 """
122 """
206 intercepted = False
123 intercepted = False
207 cursor = self.textCursor()
124 cursor = self.textCursor()
208 position = cursor.position()
125 position = cursor.position()
209 key = event.key()
126 key = event.key()
210 ctrl_down = self._control_down(event.modifiers())
127 ctrl_down = self._control_down(event.modifiers())
211 alt_down = event.modifiers() & QtCore.Qt.AltModifier
128 alt_down = event.modifiers() & QtCore.Qt.AltModifier
212 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
129 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
213
130
214 # Even though we have reimplemented 'paste', the C++ level slot is still
131 # Even though we have reimplemented 'paste', the C++ level slot is still
215 # called by Qt. So we intercept the key press here.
132 # called by Qt. So we intercept the key press here.
216 if event.matches(QtGui.QKeySequence.Paste):
133 if event.matches(QtGui.QKeySequence.Paste):
217 self.paste()
134 self.paste()
218 intercepted = True
135 intercepted = True
219
136
220 elif ctrl_down:
137 elif ctrl_down:
221 if key in self._ctrl_down_remap:
138 if key in self._ctrl_down_remap:
222 ctrl_down = False
139 ctrl_down = False
223 key = self._ctrl_down_remap[key]
140 key = self._ctrl_down_remap[key]
224 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
141 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
225 QtCore.Qt.NoModifier)
142 QtCore.Qt.NoModifier)
226
143
227 elif key == QtCore.Qt.Key_K:
144 elif key == QtCore.Qt.Key_K:
228 if self._in_buffer(position):
145 if self._in_buffer(position):
229 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
146 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
230 QtGui.QTextCursor.KeepAnchor)
147 QtGui.QTextCursor.KeepAnchor)
231 cursor.removeSelectedText()
148 cursor.removeSelectedText()
232 intercepted = True
149 intercepted = True
233
150
234 elif key == QtCore.Qt.Key_X:
151 elif key == QtCore.Qt.Key_X:
235 intercepted = True
152 intercepted = True
236
153
237 elif key == QtCore.Qt.Key_Y:
154 elif key == QtCore.Qt.Key_Y:
238 self.paste()
155 self.paste()
239 intercepted = True
156 intercepted = True
240
157
241 elif alt_down:
158 elif alt_down:
242 if key == QtCore.Qt.Key_B:
159 if key == QtCore.Qt.Key_B:
243 self.setTextCursor(self._get_word_start_cursor(position))
160 self.setTextCursor(self._get_word_start_cursor(position))
244 intercepted = True
161 intercepted = True
245
162
246 elif key == QtCore.Qt.Key_F:
163 elif key == QtCore.Qt.Key_F:
247 self.setTextCursor(self._get_word_end_cursor(position))
164 self.setTextCursor(self._get_word_end_cursor(position))
248 intercepted = True
165 intercepted = True
249
166
250 elif key == QtCore.Qt.Key_Backspace:
167 elif key == QtCore.Qt.Key_Backspace:
251 cursor = self._get_word_start_cursor(position)
168 cursor = self._get_word_start_cursor(position)
252 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
169 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
253 cursor.removeSelectedText()
170 cursor.removeSelectedText()
254 intercepted = True
171 intercepted = True
255
172
256 elif key == QtCore.Qt.Key_D:
173 elif key == QtCore.Qt.Key_D:
257 cursor = self._get_word_end_cursor(position)
174 cursor = self._get_word_end_cursor(position)
258 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
175 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
259 cursor.removeSelectedText()
176 cursor.removeSelectedText()
260 intercepted = True
177 intercepted = True
261
178
262 if self._completion_widget.isVisible():
179 if self._completion_widget.isVisible():
263 self._completion_widget.keyPressEvent(event)
180 self._completion_widget.keyPressEvent(event)
264 intercepted = event.isAccepted()
181 intercepted = event.isAccepted()
265
182
266 else:
183 else:
267 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
184 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
268 if self._reading:
185 if self._reading:
269 self.appendPlainText('\n')
186 self.appendPlainText('\n')
270 self._reading = False
187 self._reading = False
271 if self._reading_callback:
188 if self._reading_callback:
272 self._reading_callback()
189 self._reading_callback()
273 elif not self._executing:
190 elif not self._executing:
274 self.execute(interactive=True)
191 self.execute(interactive=True)
275 intercepted = True
192 intercepted = True
276
193
277 elif key == QtCore.Qt.Key_Up:
194 elif key == QtCore.Qt.Key_Up:
278 if self._reading or not self._up_pressed():
195 if self._reading or not self._up_pressed():
279 intercepted = True
196 intercepted = True
280 else:
197 else:
281 prompt_line = self._get_prompt_cursor().blockNumber()
198 prompt_line = self._get_prompt_cursor().blockNumber()
282 intercepted = cursor.blockNumber() <= prompt_line
199 intercepted = cursor.blockNumber() <= prompt_line
283
200
284 elif key == QtCore.Qt.Key_Down:
201 elif key == QtCore.Qt.Key_Down:
285 if self._reading or not self._down_pressed():
202 if self._reading or not self._down_pressed():
286 intercepted = True
203 intercepted = True
287 else:
204 else:
288 end_line = self._get_end_cursor().blockNumber()
205 end_line = self._get_end_cursor().blockNumber()
289 intercepted = cursor.blockNumber() == end_line
206 intercepted = cursor.blockNumber() == end_line
290
207
291 elif key == QtCore.Qt.Key_Tab:
208 elif key == QtCore.Qt.Key_Tab:
292 if self._reading:
209 if self._reading:
293 intercepted = False
210 intercepted = False
294 else:
211 else:
295 intercepted = not self._tab_pressed()
212 intercepted = not self._tab_pressed()
296
213
297 elif key == QtCore.Qt.Key_Left:
214 elif key == QtCore.Qt.Key_Left:
298 intercepted = not self._in_buffer(position - 1)
215 intercepted = not self._in_buffer(position - 1)
299
216
300 elif key == QtCore.Qt.Key_Home:
217 elif key == QtCore.Qt.Key_Home:
301 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
218 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
302 start_pos = cursor.position()
219 start_pos = cursor.position()
303 start_line = cursor.blockNumber()
220 start_line = cursor.blockNumber()
304 if start_line == self._get_prompt_cursor().blockNumber():
221 if start_line == self._get_prompt_cursor().blockNumber():
305 start_pos += len(self._prompt)
222 start_pos += len(self._prompt)
306 else:
223 else:
307 start_pos += len(self._continuation_prompt)
224 start_pos += len(self._continuation_prompt)
308 if shift_down and self._in_buffer(position):
225 if shift_down and self._in_buffer(position):
309 self._set_selection(position, start_pos)
226 self._set_selection(position, start_pos)
310 else:
227 else:
311 self._set_position(start_pos)
228 self._set_position(start_pos)
312 intercepted = True
229 intercepted = True
313
230
314 elif key == QtCore.Qt.Key_Backspace and not alt_down:
231 elif key == QtCore.Qt.Key_Backspace and not alt_down:
315
232
316 # Line deletion (remove continuation prompt)
233 # Line deletion (remove continuation prompt)
317 len_prompt = len(self._continuation_prompt)
234 len_prompt = len(self._continuation_prompt)
318 if not self._reading and \
235 if not self._reading and \
319 cursor.columnNumber() == len_prompt and \
236 cursor.columnNumber() == len_prompt and \
320 position != self._prompt_pos:
237 position != self._prompt_pos:
321 cursor.setPosition(position - len_prompt,
238 cursor.setPosition(position - len_prompt,
322 QtGui.QTextCursor.KeepAnchor)
239 QtGui.QTextCursor.KeepAnchor)
323 cursor.removeSelectedText()
240 cursor.removeSelectedText()
324
241
325 # Regular backwards deletion
242 # Regular backwards deletion
326 else:
243 else:
327 anchor = cursor.anchor()
244 anchor = cursor.anchor()
328 if anchor == position:
245 if anchor == position:
329 intercepted = not self._in_buffer(position - 1)
246 intercepted = not self._in_buffer(position - 1)
330 else:
247 else:
331 intercepted = not self._in_buffer(min(anchor, position))
248 intercepted = not self._in_buffer(min(anchor, position))
332
249
333 elif key == QtCore.Qt.Key_Delete:
250 elif key == QtCore.Qt.Key_Delete:
334 anchor = cursor.anchor()
251 anchor = cursor.anchor()
335 intercepted = not self._in_buffer(min(anchor, position))
252 intercepted = not self._in_buffer(min(anchor, position))
336
253
337 # Don't move cursor if control is down to allow copy-paste using
254 # Don't move cursor if control is down to allow copy-paste using
338 # the keyboard in any part of the buffer.
255 # the keyboard in any part of the buffer.
339 if not ctrl_down:
256 if not ctrl_down:
340 self._keep_cursor_in_buffer()
257 self._keep_cursor_in_buffer()
341
258
342 if not intercepted:
259 if not intercepted:
343 QtGui.QPlainTextEdit.keyPressEvent(self, event)
260 QtGui.QPlainTextEdit.keyPressEvent(self, event)
344
261
345 #--------------------------------------------------------------------------
262 #--------------------------------------------------------------------------
346 # 'QPlainTextEdit' interface
263 # 'QPlainTextEdit' interface
347 #--------------------------------------------------------------------------
264 #--------------------------------------------------------------------------
348
265
349 def appendHtml(self, html):
266 def appendHtml(self, html):
350 """ Reimplemented to not append HTML as a new paragraph, which doesn't
267 """ Reimplemented to not append HTML as a new paragraph, which doesn't
351 make sense for a console widget.
268 make sense for a console widget.
352 """
269 """
353 cursor = self._get_end_cursor()
270 cursor = self._get_end_cursor()
354 cursor.insertHtml(html)
271 cursor.insertHtml(html)
355
272
356 # After appending HTML, the text document "remembers" the current
273 # After appending HTML, the text document "remembers" the current
357 # formatting, which means that subsequent calls to 'appendPlainText'
274 # formatting, which means that subsequent calls to 'appendPlainText'
358 # will be formatted similarly, a behavior that we do not want. To
275 # will be formatted similarly, a behavior that we do not want. To
359 # prevent this, we make sure that the last character has no formatting.
276 # prevent this, we make sure that the last character has no formatting.
360 cursor.movePosition(QtGui.QTextCursor.Left,
277 cursor.movePosition(QtGui.QTextCursor.Left,
361 QtGui.QTextCursor.KeepAnchor)
278 QtGui.QTextCursor.KeepAnchor)
362 if cursor.selection().toPlainText().trimmed().isEmpty():
279 if cursor.selection().toPlainText().trimmed().isEmpty():
363 # If the last character is whitespace, it doesn't matter how it's
280 # If the last character is whitespace, it doesn't matter how it's
364 # formatted, so just clear the formatting.
281 # formatted, so just clear the formatting.
365 cursor.setCharFormat(QtGui.QTextCharFormat())
282 cursor.setCharFormat(QtGui.QTextCharFormat())
366 else:
283 else:
367 # Otherwise, add an unformatted space.
284 # Otherwise, add an unformatted space.
368 cursor.movePosition(QtGui.QTextCursor.Right)
285 cursor.movePosition(QtGui.QTextCursor.Right)
369 cursor.insertText(' ', QtGui.QTextCharFormat())
286 cursor.insertText(' ', QtGui.QTextCharFormat())
370
287
371 def appendPlainText(self, text):
288 def appendPlainText(self, text):
372 """ Reimplemented to not append text as a new paragraph, which doesn't
289 """ Reimplemented to not append text as a new paragraph, which doesn't
373 make sense for a console widget. Also, if enabled, handle ANSI
290 make sense for a console widget. Also, if enabled, handle ANSI
374 codes.
291 codes.
375 """
292 """
376 cursor = self._get_end_cursor()
293 cursor = self._get_end_cursor()
377 if self.ansi_codes:
294 if self.ansi_codes:
378 format = QtGui.QTextCharFormat()
295 for substring in self._ansi_processor.split_string(text):
379 previous_end = 0
380 for match in self._ansi_pattern.finditer(text):
381 cursor.insertText(text[previous_end:match.start()], format)
382 previous_end = match.end()
383 for code in match.group(1).split(';'):
384 self._ansi_processor.set_code(int(code))
385 format = self._ansi_processor.get_format()
296 format = self._ansi_processor.get_format()
386 cursor.insertText(text[previous_end:], format)
297 cursor.insertText(substring, format)
387 else:
298 else:
388 cursor.insertText(text)
299 cursor.insertText(text)
389
300
390 def clear(self, keep_input=False):
301 def clear(self, keep_input=False):
391 """ Reimplemented to write a new prompt. If 'keep_input' is set,
302 """ Reimplemented to write a new prompt. If 'keep_input' is set,
392 restores the old input buffer when the new prompt is written.
303 restores the old input buffer when the new prompt is written.
393 """
304 """
394 QtGui.QPlainTextEdit.clear(self)
305 QtGui.QPlainTextEdit.clear(self)
395 if keep_input:
306 if keep_input:
396 input_buffer = self.input_buffer
307 input_buffer = self.input_buffer
397 self._show_prompt()
308 self._show_prompt()
398 if keep_input:
309 if keep_input:
399 self.input_buffer = input_buffer
310 self.input_buffer = input_buffer
400
311
401 def paste(self):
312 def paste(self):
402 """ Reimplemented to ensure that text is pasted in the editing region.
313 """ Reimplemented to ensure that text is pasted in the editing region.
403 """
314 """
404 self._keep_cursor_in_buffer()
315 self._keep_cursor_in_buffer()
405 QtGui.QPlainTextEdit.paste(self)
316 QtGui.QPlainTextEdit.paste(self)
406
317
407 def print_(self, printer):
318 def print_(self, printer):
408 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
319 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
409 slot has the wrong signature.
320 slot has the wrong signature.
410 """
321 """
411 QtGui.QPlainTextEdit.print_(self, printer)
322 QtGui.QPlainTextEdit.print_(self, printer)
412
323
413 #---------------------------------------------------------------------------
324 #---------------------------------------------------------------------------
414 # 'ConsoleWidget' public interface
325 # 'ConsoleWidget' public interface
415 #---------------------------------------------------------------------------
326 #---------------------------------------------------------------------------
416
327
417 def execute(self, source=None, hidden=False, interactive=False):
328 def execute(self, source=None, hidden=False, interactive=False):
418 """ Executes source or the input buffer, possibly prompting for more
329 """ Executes source or the input buffer, possibly prompting for more
419 input.
330 input.
420
331
421 Parameters:
332 Parameters:
422 -----------
333 -----------
423 source : str, optional
334 source : str, optional
424
335
425 The source to execute. If not specified, the input buffer will be
336 The source to execute. If not specified, the input buffer will be
426 used. If specified and 'hidden' is False, the input buffer will be
337 used. If specified and 'hidden' is False, the input buffer will be
427 replaced with the source before execution.
338 replaced with the source before execution.
428
339
429 hidden : bool, optional (default False)
340 hidden : bool, optional (default False)
430
341
431 If set, no output will be shown and the prompt will not be modified.
342 If set, no output will be shown and the prompt will not be modified.
432 In other words, it will be completely invisible to the user that
343 In other words, it will be completely invisible to the user that
433 an execution has occurred.
344 an execution has occurred.
434
345
435 interactive : bool, optional (default False)
346 interactive : bool, optional (default False)
436
347
437 Whether the console is to treat the source as having been manually
348 Whether the console is to treat the source as having been manually
438 entered by the user. The effect of this parameter depends on the
349 entered by the user. The effect of this parameter depends on the
439 subclass implementation.
350 subclass implementation.
440
351
441 Raises:
352 Raises:
442 -------
353 -------
443 RuntimeError
354 RuntimeError
444 If incomplete input is given and 'hidden' is True. In this case,
355 If incomplete input is given and 'hidden' is True. In this case,
445 it not possible to prompt for more input.
356 it not possible to prompt for more input.
446
357
447 Returns:
358 Returns:
448 --------
359 --------
449 A boolean indicating whether the source was executed.
360 A boolean indicating whether the source was executed.
450 """
361 """
451 if not hidden:
362 if not hidden:
452 if source is not None:
363 if source is not None:
453 self.input_buffer = source
364 self.input_buffer = source
454
365
455 self.appendPlainText('\n')
366 self.appendPlainText('\n')
456 self._executing_input_buffer = self.input_buffer
367 self._executing_input_buffer = self.input_buffer
457 self._executing = True
368 self._executing = True
458 self._prompt_finished()
369 self._prompt_finished()
459
370
460 real_source = self.input_buffer if source is None else source
371 real_source = self.input_buffer if source is None else source
461 complete = self._is_complete(real_source, interactive)
372 complete = self._is_complete(real_source, interactive)
462 if complete:
373 if complete:
463 if not hidden:
374 if not hidden:
464 # The maximum block count is only in effect during execution.
375 # The maximum block count is only in effect during execution.
465 # This ensures that _prompt_pos does not become invalid due to
376 # This ensures that _prompt_pos does not become invalid due to
466 # text truncation.
377 # text truncation.
467 self.setMaximumBlockCount(self.buffer_size)
378 self.setMaximumBlockCount(self.buffer_size)
468 self._execute(real_source, hidden)
379 self._execute(real_source, hidden)
469 elif hidden:
380 elif hidden:
470 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
381 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
471 else:
382 else:
472 self._show_continuation_prompt()
383 self._show_continuation_prompt()
473
384
474 return complete
385 return complete
475
386
476 def _get_input_buffer(self):
387 def _get_input_buffer(self):
477 """ The text that the user has entered entered at the current prompt.
388 """ The text that the user has entered entered at the current prompt.
478 """
389 """
479 # If we're executing, the input buffer may not even exist anymore due to
390 # If we're executing, the input buffer may not even exist anymore due to
480 # the limit imposed by 'buffer_size'. Therefore, we store it.
391 # the limit imposed by 'buffer_size'. Therefore, we store it.
481 if self._executing:
392 if self._executing:
482 return self._executing_input_buffer
393 return self._executing_input_buffer
483
394
484 cursor = self._get_end_cursor()
395 cursor = self._get_end_cursor()
485 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
396 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
486 input_buffer = str(cursor.selection().toPlainText())
397 input_buffer = str(cursor.selection().toPlainText())
487
398
488 # Strip out continuation prompts.
399 # Strip out continuation prompts.
489 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
400 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
490
401
491 def _set_input_buffer(self, string):
402 def _set_input_buffer(self, string):
492 """ Replaces the text in the input buffer with 'string'.
403 """ Replaces the text in the input buffer with 'string'.
493 """
404 """
494 # Add continuation prompts where necessary.
405 # Add continuation prompts where necessary.
495 lines = string.splitlines()
406 lines = string.splitlines()
496 for i in xrange(1, len(lines)):
407 for i in xrange(1, len(lines)):
497 lines[i] = self._continuation_prompt + lines[i]
408 lines[i] = self._continuation_prompt + lines[i]
498 string = '\n'.join(lines)
409 string = '\n'.join(lines)
499
410
500 # Replace buffer with new text.
411 # Replace buffer with new text.
501 cursor = self._get_end_cursor()
412 cursor = self._get_end_cursor()
502 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
413 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
503 cursor.insertText(string)
414 cursor.insertText(string)
504 self.moveCursor(QtGui.QTextCursor.End)
415 self.moveCursor(QtGui.QTextCursor.End)
505
416
506 input_buffer = property(_get_input_buffer, _set_input_buffer)
417 input_buffer = property(_get_input_buffer, _set_input_buffer)
507
418
508 def _get_input_buffer_cursor_line(self):
419 def _get_input_buffer_cursor_line(self):
509 """ The text in the line of the input buffer in which the user's cursor
420 """ The text in the line of the input buffer in which the user's cursor
510 rests. Returns a string if there is such a line; otherwise, None.
421 rests. Returns a string if there is such a line; otherwise, None.
511 """
422 """
512 if self._executing:
423 if self._executing:
513 return None
424 return None
514 cursor = self.textCursor()
425 cursor = self.textCursor()
515 if cursor.position() >= self._prompt_pos:
426 if cursor.position() >= self._prompt_pos:
516 text = self._get_block_plain_text(cursor.block())
427 text = self._get_block_plain_text(cursor.block())
517 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
428 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
518 return text[len(self._prompt):]
429 return text[len(self._prompt):]
519 else:
430 else:
520 return text[len(self._continuation_prompt):]
431 return text[len(self._continuation_prompt):]
521 else:
432 else:
522 return None
433 return None
523
434
524 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
435 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
525
436
526 def _get_font(self):
437 def _get_font(self):
527 """ The base font being used by the ConsoleWidget.
438 """ The base font being used by the ConsoleWidget.
528 """
439 """
529 return self.document().defaultFont()
440 return self.document().defaultFont()
530
441
531 def _set_font(self, font):
442 def _set_font(self, font):
532 """ Sets the base font for the ConsoleWidget to the specified QFont.
443 """ Sets the base font for the ConsoleWidget to the specified QFont.
533 """
444 """
445 font_metrics = QtGui.QFontMetrics(font)
446 self.setTabStopWidth(self.tab_width * font_metrics.width(' '))
447
534 self._completion_widget.setFont(font)
448 self._completion_widget.setFont(font)
535 self.document().setDefaultFont(font)
449 self.document().setDefaultFont(font)
536
450
537 font = property(_get_font, _set_font)
451 font = property(_get_font, _set_font)
538
452
539 def reset_font(self):
453 def reset_font(self):
540 """ Sets the font to the default fixed-width font for this platform.
454 """ Sets the font to the default fixed-width font for this platform.
541 """
455 """
542 if sys.platform == 'win32':
456 if sys.platform == 'win32':
543 name = 'Courier'
457 name = 'Courier'
544 elif sys.platform == 'darwin':
458 elif sys.platform == 'darwin':
545 name = 'Monaco'
459 name = 'Monaco'
546 else:
460 else:
547 name = 'Monospace'
461 name = 'Monospace'
548 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
462 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
549 font.setStyleHint(QtGui.QFont.TypeWriter)
463 font.setStyleHint(QtGui.QFont.TypeWriter)
550 self._set_font(font)
464 self._set_font(font)
551
465
552 #---------------------------------------------------------------------------
466 #---------------------------------------------------------------------------
553 # 'ConsoleWidget' abstract interface
467 # 'ConsoleWidget' abstract interface
554 #---------------------------------------------------------------------------
468 #---------------------------------------------------------------------------
555
469
556 def _is_complete(self, source, interactive):
470 def _is_complete(self, source, interactive):
557 """ Returns whether 'source' can be executed. When triggered by an
471 """ Returns whether 'source' can be executed. When triggered by an
558 Enter/Return key press, 'interactive' is True; otherwise, it is
472 Enter/Return key press, 'interactive' is True; otherwise, it is
559 False.
473 False.
560 """
474 """
561 raise NotImplementedError
475 raise NotImplementedError
562
476
563 def _execute(self, source, hidden):
477 def _execute(self, source, hidden):
564 """ Execute 'source'. If 'hidden', do not show any output.
478 """ Execute 'source'. If 'hidden', do not show any output.
565 """
479 """
566 raise NotImplementedError
480 raise NotImplementedError
567
481
568 def _prompt_started_hook(self):
482 def _prompt_started_hook(self):
569 """ Called immediately after a new prompt is displayed.
483 """ Called immediately after a new prompt is displayed.
570 """
484 """
571 pass
485 pass
572
486
573 def _prompt_finished_hook(self):
487 def _prompt_finished_hook(self):
574 """ Called immediately after a prompt is finished, i.e. when some input
488 """ Called immediately after a prompt is finished, i.e. when some input
575 will be processed and a new prompt displayed.
489 will be processed and a new prompt displayed.
576 """
490 """
577 pass
491 pass
578
492
579 def _up_pressed(self):
493 def _up_pressed(self):
580 """ Called when the up key is pressed. Returns whether to continue
494 """ Called when the up key is pressed. Returns whether to continue
581 processing the event.
495 processing the event.
582 """
496 """
583 return True
497 return True
584
498
585 def _down_pressed(self):
499 def _down_pressed(self):
586 """ Called when the down key is pressed. Returns whether to continue
500 """ Called when the down key is pressed. Returns whether to continue
587 processing the event.
501 processing the event.
588 """
502 """
589 return True
503 return True
590
504
591 def _tab_pressed(self):
505 def _tab_pressed(self):
592 """ Called when the tab key is pressed. Returns whether to continue
506 """ Called when the tab key is pressed. Returns whether to continue
593 processing the event.
507 processing the event.
594 """
508 """
595 return False
509 return False
596
510
597 #--------------------------------------------------------------------------
511 #--------------------------------------------------------------------------
598 # 'ConsoleWidget' protected interface
512 # 'ConsoleWidget' protected interface
599 #--------------------------------------------------------------------------
513 #--------------------------------------------------------------------------
600
514
601 def _append_html_fetching_plain_text(self, html):
515 def _append_html_fetching_plain_text(self, html):
602 """ Appends 'html', then returns the plain text version of it.
516 """ Appends 'html', then returns the plain text version of it.
603 """
517 """
604 anchor = self._get_end_cursor().position()
518 anchor = self._get_end_cursor().position()
605 self.appendHtml(html)
519 self.appendHtml(html)
606 cursor = self._get_end_cursor()
520 cursor = self._get_end_cursor()
607 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
521 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
608 return str(cursor.selection().toPlainText())
522 return str(cursor.selection().toPlainText())
609
523
610 def _append_plain_text_keeping_prompt(self, text):
524 def _append_plain_text_keeping_prompt(self, text):
611 """ Writes 'text' after the current prompt, then restores the old prompt
525 """ Writes 'text' after the current prompt, then restores the old prompt
612 with its old input buffer.
526 with its old input buffer.
613 """
527 """
614 input_buffer = self.input_buffer
528 input_buffer = self.input_buffer
615 self.appendPlainText('\n')
529 self.appendPlainText('\n')
616 self._prompt_finished()
530 self._prompt_finished()
617
531
618 self.appendPlainText(text)
532 self.appendPlainText(text)
619 self._show_prompt()
533 self._show_prompt()
620 self.input_buffer = input_buffer
534 self.input_buffer = input_buffer
621
535
622 def _control_down(self, modifiers):
536 def _control_down(self, modifiers):
623 """ Given a KeyboardModifiers flags object, return whether the Control
537 """ Given a KeyboardModifiers flags object, return whether the Control
624 key is down (on Mac OS, treat the Command key as a synonym for
538 key is down (on Mac OS, treat the Command key as a synonym for
625 Control).
539 Control).
626 """
540 """
627 down = bool(modifiers & QtCore.Qt.ControlModifier)
541 down = bool(modifiers & QtCore.Qt.ControlModifier)
628
542
629 # Note: on Mac OS, ControlModifier corresponds to the Command key while
543 # Note: on Mac OS, ControlModifier corresponds to the Command key while
630 # MetaModifier corresponds to the Control key.
544 # MetaModifier corresponds to the Control key.
631 if sys.platform == 'darwin':
545 if sys.platform == 'darwin':
632 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
546 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
633
547
634 return down
548 return down
635
549
636 def _complete_with_items(self, cursor, items):
550 def _complete_with_items(self, cursor, items):
637 """ Performs completion with 'items' at the specified cursor location.
551 """ Performs completion with 'items' at the specified cursor location.
638 """
552 """
639 if len(items) == 1:
553 if len(items) == 1:
640 cursor.setPosition(self.textCursor().position(),
554 cursor.setPosition(self.textCursor().position(),
641 QtGui.QTextCursor.KeepAnchor)
555 QtGui.QTextCursor.KeepAnchor)
642 cursor.insertText(items[0])
556 cursor.insertText(items[0])
643 elif len(items) > 1:
557 elif len(items) > 1:
644 if self.gui_completion:
558 if self.gui_completion:
645 self._completion_widget.show_items(cursor, items)
559 self._completion_widget.show_items(cursor, items)
646 else:
560 else:
647 text = '\n'.join(items) + '\n'
561 text = '\n'.join(items) + '\n'
648 self._append_plain_text_keeping_prompt(text)
562 self._append_plain_text_keeping_prompt(text)
649
563
650 def _get_block_plain_text(self, block):
564 def _get_block_plain_text(self, block):
651 """ Given a QTextBlock, return its unformatted text.
565 """ Given a QTextBlock, return its unformatted text.
652 """
566 """
653 cursor = QtGui.QTextCursor(block)
567 cursor = QtGui.QTextCursor(block)
654 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
568 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
655 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
569 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
656 QtGui.QTextCursor.KeepAnchor)
570 QtGui.QTextCursor.KeepAnchor)
657 return str(cursor.selection().toPlainText())
571 return str(cursor.selection().toPlainText())
658
572
659 def _get_end_cursor(self):
573 def _get_end_cursor(self):
660 """ Convenience method that returns a cursor for the last character.
574 """ Convenience method that returns a cursor for the last character.
661 """
575 """
662 cursor = self.textCursor()
576 cursor = self.textCursor()
663 cursor.movePosition(QtGui.QTextCursor.End)
577 cursor.movePosition(QtGui.QTextCursor.End)
664 return cursor
578 return cursor
665
579
666 def _get_prompt_cursor(self):
580 def _get_prompt_cursor(self):
667 """ Convenience method that returns a cursor for the prompt position.
581 """ Convenience method that returns a cursor for the prompt position.
668 """
582 """
669 cursor = self.textCursor()
583 cursor = self.textCursor()
670 cursor.setPosition(self._prompt_pos)
584 cursor.setPosition(self._prompt_pos)
671 return cursor
585 return cursor
672
586
673 def _get_selection_cursor(self, start, end):
587 def _get_selection_cursor(self, start, end):
674 """ Convenience method that returns a cursor with text selected between
588 """ Convenience method that returns a cursor with text selected between
675 the positions 'start' and 'end'.
589 the positions 'start' and 'end'.
676 """
590 """
677 cursor = self.textCursor()
591 cursor = self.textCursor()
678 cursor.setPosition(start)
592 cursor.setPosition(start)
679 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
593 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
680 return cursor
594 return cursor
681
595
682 def _get_word_start_cursor(self, position):
596 def _get_word_start_cursor(self, position):
683 """ Find the start of the word to the left the given position. If a
597 """ Find the start of the word to the left the given position. If a
684 sequence of non-word characters precedes the first word, skip over
598 sequence of non-word characters precedes the first word, skip over
685 them. (This emulates the behavior of bash, emacs, etc.)
599 them. (This emulates the behavior of bash, emacs, etc.)
686 """
600 """
687 document = self.document()
601 document = self.document()
688 position -= 1
602 position -= 1
689 while self._in_buffer(position) and \
603 while self._in_buffer(position) and \
690 not document.characterAt(position).isLetterOrNumber():
604 not document.characterAt(position).isLetterOrNumber():
691 position -= 1
605 position -= 1
692 while self._in_buffer(position) and \
606 while self._in_buffer(position) and \
693 document.characterAt(position).isLetterOrNumber():
607 document.characterAt(position).isLetterOrNumber():
694 position -= 1
608 position -= 1
695 cursor = self.textCursor()
609 cursor = self.textCursor()
696 cursor.setPosition(position + 1)
610 cursor.setPosition(position + 1)
697 return cursor
611 return cursor
698
612
699 def _get_word_end_cursor(self, position):
613 def _get_word_end_cursor(self, position):
700 """ Find the end of the word to the right the given position. If a
614 """ Find the end of the word to the right the given position. If a
701 sequence of non-word characters precedes the first word, skip over
615 sequence of non-word characters precedes the first word, skip over
702 them. (This emulates the behavior of bash, emacs, etc.)
616 them. (This emulates the behavior of bash, emacs, etc.)
703 """
617 """
704 document = self.document()
618 document = self.document()
705 end = self._get_end_cursor().position()
619 end = self._get_end_cursor().position()
706 while position < end and \
620 while position < end and \
707 not document.characterAt(position).isLetterOrNumber():
621 not document.characterAt(position).isLetterOrNumber():
708 position += 1
622 position += 1
709 while position < end and \
623 while position < end and \
710 document.characterAt(position).isLetterOrNumber():
624 document.characterAt(position).isLetterOrNumber():
711 position += 1
625 position += 1
712 cursor = self.textCursor()
626 cursor = self.textCursor()
713 cursor.setPosition(position)
627 cursor.setPosition(position)
714 return cursor
628 return cursor
715
629
716 def _prompt_started(self):
630 def _prompt_started(self):
717 """ Called immediately after a new prompt is displayed.
631 """ Called immediately after a new prompt is displayed.
718 """
632 """
719 # Temporarily disable the maximum block count to permit undo/redo and
633 # Temporarily disable the maximum block count to permit undo/redo and
720 # to ensure that the prompt position does not change due to truncation.
634 # to ensure that the prompt position does not change due to truncation.
721 self.setMaximumBlockCount(0)
635 self.setMaximumBlockCount(0)
722 self.setUndoRedoEnabled(True)
636 self.setUndoRedoEnabled(True)
723
637
724 self.setReadOnly(False)
638 self.setReadOnly(False)
725 self.moveCursor(QtGui.QTextCursor.End)
639 self.moveCursor(QtGui.QTextCursor.End)
726 self.centerCursor()
640 self.centerCursor()
727
641
728 self._executing = False
642 self._executing = False
729 self._prompt_started_hook()
643 self._prompt_started_hook()
730
644
731 def _prompt_finished(self):
645 def _prompt_finished(self):
732 """ Called immediately after a prompt is finished, i.e. when some input
646 """ Called immediately after a prompt is finished, i.e. when some input
733 will be processed and a new prompt displayed.
647 will be processed and a new prompt displayed.
734 """
648 """
735 self.setUndoRedoEnabled(False)
649 self.setUndoRedoEnabled(False)
736 self.setReadOnly(True)
650 self.setReadOnly(True)
737 self._prompt_finished_hook()
651 self._prompt_finished_hook()
738
652
739 def _readline(self, prompt='', callback=None):
653 def _readline(self, prompt='', callback=None):
740 """ Reads one line of input from the user.
654 """ Reads one line of input from the user.
741
655
742 Parameters
656 Parameters
743 ----------
657 ----------
744 prompt : str, optional
658 prompt : str, optional
745 The prompt to print before reading the line.
659 The prompt to print before reading the line.
746
660
747 callback : callable, optional
661 callback : callable, optional
748 A callback to execute with the read line. If not specified, input is
662 A callback to execute with the read line. If not specified, input is
749 read *synchronously* and this method does not return until it has
663 read *synchronously* and this method does not return until it has
750 been read.
664 been read.
751
665
752 Returns
666 Returns
753 -------
667 -------
754 If a callback is specified, returns nothing. Otherwise, returns the
668 If a callback is specified, returns nothing. Otherwise, returns the
755 input string with the trailing newline stripped.
669 input string with the trailing newline stripped.
756 """
670 """
757 if self._reading:
671 if self._reading:
758 raise RuntimeError('Cannot read a line. Widget is already reading.')
672 raise RuntimeError('Cannot read a line. Widget is already reading.')
759
673
760 if not callback and not self.isVisible():
674 if not callback and not self.isVisible():
761 # If the user cannot see the widget, this function cannot return.
675 # If the user cannot see the widget, this function cannot return.
762 raise RuntimeError('Cannot synchronously read a line if the widget'
676 raise RuntimeError('Cannot synchronously read a line if the widget'
763 'is not visible!')
677 'is not visible!')
764
678
765 self._reading = True
679 self._reading = True
766 self._show_prompt(prompt, newline=False)
680 self._show_prompt(prompt, newline=False)
767
681
768 if callback is None:
682 if callback is None:
769 self._reading_callback = None
683 self._reading_callback = None
770 while self._reading:
684 while self._reading:
771 QtCore.QCoreApplication.processEvents()
685 QtCore.QCoreApplication.processEvents()
772 return self.input_buffer.rstrip('\n')
686 return self.input_buffer.rstrip('\n')
773
687
774 else:
688 else:
775 self._reading_callback = lambda: \
689 self._reading_callback = lambda: \
776 callback(self.input_buffer.rstrip('\n'))
690 callback(self.input_buffer.rstrip('\n'))
777
691
778 def _reset(self):
692 def _reset(self):
779 """ Clears the console and resets internal state variables.
693 """ Clears the console and resets internal state variables.
780 """
694 """
781 QtGui.QPlainTextEdit.clear(self)
695 QtGui.QPlainTextEdit.clear(self)
782 self._executing = self._reading = False
696 self._executing = self._reading = False
783
697
784 def _set_continuation_prompt(self, prompt, html=False):
698 def _set_continuation_prompt(self, prompt, html=False):
785 """ Sets the continuation prompt.
699 """ Sets the continuation prompt.
786
700
787 Parameters
701 Parameters
788 ----------
702 ----------
789 prompt : str
703 prompt : str
790 The prompt to show when more input is needed.
704 The prompt to show when more input is needed.
791
705
792 html : bool, optional (default False)
706 html : bool, optional (default False)
793 If set, the prompt will be inserted as formatted HTML. Otherwise,
707 If set, the prompt will be inserted as formatted HTML. Otherwise,
794 the prompt will be treated as plain text, though ANSI color codes
708 the prompt will be treated as plain text, though ANSI color codes
795 will be handled.
709 will be handled.
796 """
710 """
797 if html:
711 if html:
798 self._continuation_prompt_html = prompt
712 self._continuation_prompt_html = prompt
799 else:
713 else:
800 self._continuation_prompt = prompt
714 self._continuation_prompt = prompt
801 self._continuation_prompt_html = None
715 self._continuation_prompt_html = None
802
716
803 def _set_position(self, position):
717 def _set_position(self, position):
804 """ Convenience method to set the position of the cursor.
718 """ Convenience method to set the position of the cursor.
805 """
719 """
806 cursor = self.textCursor()
720 cursor = self.textCursor()
807 cursor.setPosition(position)
721 cursor.setPosition(position)
808 self.setTextCursor(cursor)
722 self.setTextCursor(cursor)
809
723
810 def _set_selection(self, start, end):
724 def _set_selection(self, start, end):
811 """ Convenience method to set the current selected text.
725 """ Convenience method to set the current selected text.
812 """
726 """
813 self.setTextCursor(self._get_selection_cursor(start, end))
727 self.setTextCursor(self._get_selection_cursor(start, end))
814
728
815 def _show_prompt(self, prompt=None, html=False, newline=True):
729 def _show_prompt(self, prompt=None, html=False, newline=True):
816 """ Writes a new prompt at the end of the buffer.
730 """ Writes a new prompt at the end of the buffer.
817
731
818 Parameters
732 Parameters
819 ----------
733 ----------
820 prompt : str, optional
734 prompt : str, optional
821 The prompt to show. If not specified, the previous prompt is used.
735 The prompt to show. If not specified, the previous prompt is used.
822
736
823 html : bool, optional (default False)
737 html : bool, optional (default False)
824 Only relevant when a prompt is specified. If set, the prompt will
738 Only relevant when a prompt is specified. If set, the prompt will
825 be inserted as formatted HTML. Otherwise, the prompt will be treated
739 be inserted as formatted HTML. Otherwise, the prompt will be treated
826 as plain text, though ANSI color codes will be handled.
740 as plain text, though ANSI color codes will be handled.
827
741
828 newline : bool, optional (default True)
742 newline : bool, optional (default True)
829 If set, a new line will be written before showing the prompt if
743 If set, a new line will be written before showing the prompt if
830 there is not already a newline at the end of the buffer.
744 there is not already a newline at the end of the buffer.
831 """
745 """
832 # Insert a preliminary newline, if necessary.
746 # Insert a preliminary newline, if necessary.
833 if newline:
747 if newline:
834 cursor = self._get_end_cursor()
748 cursor = self._get_end_cursor()
835 if cursor.position() > 0:
749 if cursor.position() > 0:
836 cursor.movePosition(QtGui.QTextCursor.Left,
750 cursor.movePosition(QtGui.QTextCursor.Left,
837 QtGui.QTextCursor.KeepAnchor)
751 QtGui.QTextCursor.KeepAnchor)
838 if str(cursor.selection().toPlainText()) != '\n':
752 if str(cursor.selection().toPlainText()) != '\n':
839 self.appendPlainText('\n')
753 self.appendPlainText('\n')
840
754
841 # Write the prompt.
755 # Write the prompt.
842 if prompt is None:
756 if prompt is None:
843 if self._prompt_html is None:
757 if self._prompt_html is None:
844 self.appendPlainText(self._prompt)
758 self.appendPlainText(self._prompt)
845 else:
759 else:
846 self.appendHtml(self._prompt_html)
760 self.appendHtml(self._prompt_html)
847 else:
761 else:
848 if html:
762 if html:
849 self._prompt = self._append_html_fetching_plain_text(prompt)
763 self._prompt = self._append_html_fetching_plain_text(prompt)
850 self._prompt_html = prompt
764 self._prompt_html = prompt
851 else:
765 else:
852 self.appendPlainText(prompt)
766 self.appendPlainText(prompt)
853 self._prompt = prompt
767 self._prompt = prompt
854 self._prompt_html = None
768 self._prompt_html = None
855
769
856 self._prompt_pos = self._get_end_cursor().position()
770 self._prompt_pos = self._get_end_cursor().position()
857 self._prompt_started()
771 self._prompt_started()
858
772
859 def _show_continuation_prompt(self):
773 def _show_continuation_prompt(self):
860 """ Writes a new continuation prompt at the end of the buffer.
774 """ Writes a new continuation prompt at the end of the buffer.
861 """
775 """
862 if self._continuation_prompt_html is None:
776 if self._continuation_prompt_html is None:
863 self.appendPlainText(self._continuation_prompt)
777 self.appendPlainText(self._continuation_prompt)
864 else:
778 else:
865 self._continuation_prompt = self._append_html_fetching_plain_text(
779 self._continuation_prompt = self._append_html_fetching_plain_text(
866 self._continuation_prompt_html)
780 self._continuation_prompt_html)
867
781
868 self._prompt_started()
782 self._prompt_started()
869
783
870 def _in_buffer(self, position):
784 def _in_buffer(self, position):
871 """ Returns whether the given position is inside the editing region.
785 """ Returns whether the given position is inside the editing region.
872 """
786 """
873 return position >= self._prompt_pos
787 return position >= self._prompt_pos
874
788
875 def _keep_cursor_in_buffer(self):
789 def _keep_cursor_in_buffer(self):
876 """ Ensures that the cursor is inside the editing region. Returns
790 """ Ensures that the cursor is inside the editing region. Returns
877 whether the cursor was moved.
791 whether the cursor was moved.
878 """
792 """
879 cursor = self.textCursor()
793 cursor = self.textCursor()
880 if cursor.position() < self._prompt_pos:
794 if cursor.position() < self._prompt_pos:
881 cursor.movePosition(QtGui.QTextCursor.End)
795 cursor.movePosition(QtGui.QTextCursor.End)
882 self.setTextCursor(cursor)
796 self.setTextCursor(cursor)
883 return True
797 return True
884 else:
798 else:
885 return False
799 return False
886
800
887
801
888 class HistoryConsoleWidget(ConsoleWidget):
802 class HistoryConsoleWidget(ConsoleWidget):
889 """ A ConsoleWidget that keeps a history of the commands that have been
803 """ A ConsoleWidget that keeps a history of the commands that have been
890 executed.
804 executed.
891 """
805 """
892
806
893 #---------------------------------------------------------------------------
807 #---------------------------------------------------------------------------
894 # 'QObject' interface
808 # 'QObject' interface
895 #---------------------------------------------------------------------------
809 #---------------------------------------------------------------------------
896
810
897 def __init__(self, parent=None):
811 def __init__(self, parent=None):
898 super(HistoryConsoleWidget, self).__init__(parent)
812 super(HistoryConsoleWidget, self).__init__(parent)
899
813
900 self._history = []
814 self._history = []
901 self._history_index = 0
815 self._history_index = 0
902
816
903 #---------------------------------------------------------------------------
817 #---------------------------------------------------------------------------
904 # 'ConsoleWidget' public interface
818 # 'ConsoleWidget' public interface
905 #---------------------------------------------------------------------------
819 #---------------------------------------------------------------------------
906
820
907 def execute(self, source=None, hidden=False, interactive=False):
821 def execute(self, source=None, hidden=False, interactive=False):
908 """ Reimplemented to the store history.
822 """ Reimplemented to the store history.
909 """
823 """
910 if not hidden:
824 if not hidden:
911 history = self.input_buffer if source is None else source
825 history = self.input_buffer if source is None else source
912
826
913 executed = super(HistoryConsoleWidget, self).execute(
827 executed = super(HistoryConsoleWidget, self).execute(
914 source, hidden, interactive)
828 source, hidden, interactive)
915
829
916 if executed and not hidden:
830 if executed and not hidden:
917 self._history.append(history.rstrip())
831 self._history.append(history.rstrip())
918 self._history_index = len(self._history)
832 self._history_index = len(self._history)
919
833
920 return executed
834 return executed
921
835
922 #---------------------------------------------------------------------------
836 #---------------------------------------------------------------------------
923 # 'ConsoleWidget' abstract interface
837 # 'ConsoleWidget' abstract interface
924 #---------------------------------------------------------------------------
838 #---------------------------------------------------------------------------
925
839
926 def _up_pressed(self):
840 def _up_pressed(self):
927 """ Called when the up key is pressed. Returns whether to continue
841 """ Called when the up key is pressed. Returns whether to continue
928 processing the event.
842 processing the event.
929 """
843 """
930 prompt_cursor = self._get_prompt_cursor()
844 prompt_cursor = self._get_prompt_cursor()
931 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
845 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
932 self.history_previous()
846 self.history_previous()
933
847
934 # Go to the first line of prompt for seemless history scrolling.
848 # Go to the first line of prompt for seemless history scrolling.
935 cursor = self._get_prompt_cursor()
849 cursor = self._get_prompt_cursor()
936 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
850 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
937 self.setTextCursor(cursor)
851 self.setTextCursor(cursor)
938
852
939 return False
853 return False
940 return True
854 return True
941
855
942 def _down_pressed(self):
856 def _down_pressed(self):
943 """ Called when the down key is pressed. Returns whether to continue
857 """ Called when the down key is pressed. Returns whether to continue
944 processing the event.
858 processing the event.
945 """
859 """
946 end_cursor = self._get_end_cursor()
860 end_cursor = self._get_end_cursor()
947 if self.textCursor().blockNumber() == end_cursor.blockNumber():
861 if self.textCursor().blockNumber() == end_cursor.blockNumber():
948 self.history_next()
862 self.history_next()
949 return False
863 return False
950 return True
864 return True
951
865
952 #---------------------------------------------------------------------------
866 #---------------------------------------------------------------------------
953 # 'HistoryConsoleWidget' interface
867 # 'HistoryConsoleWidget' interface
954 #---------------------------------------------------------------------------
868 #---------------------------------------------------------------------------
955
869
956 def history_previous(self):
870 def history_previous(self):
957 """ If possible, set the input buffer to the previous item in the
871 """ If possible, set the input buffer to the previous item in the
958 history.
872 history.
959 """
873 """
960 if self._history_index > 0:
874 if self._history_index > 0:
961 self._history_index -= 1
875 self._history_index -= 1
962 self.input_buffer = self._history[self._history_index]
876 self.input_buffer = self._history[self._history_index]
963
877
964 def history_next(self):
878 def history_next(self):
965 """ Set the input buffer to the next item in the history, or a blank
879 """ Set the input buffer to the next item in the history, or a blank
966 line if there is no subsequent item.
880 line if there is no subsequent item.
967 """
881 """
968 if self._history_index < len(self._history):
882 if self._history_index < len(self._history):
969 self._history_index += 1
883 self._history_index += 1
970 if self._history_index < len(self._history):
884 if self._history_index < len(self._history):
971 self.input_buffer = self._history[self._history_index]
885 self.input_buffer = self._history[self._history_index]
972 else:
886 else:
973 self.input_buffer = ''
887 self.input_buffer = ''
@@ -1,377 +1,380 b''
1 # Standard library imports
1 # Standard library imports
2 import signal
2 import signal
3 import sys
3 import sys
4
4
5 # System library imports
5 # System library imports
6 from pygments.lexers import PythonLexer
6 from pygments.lexers import PythonLexer
7 from PyQt4 import QtCore, QtGui
7 from PyQt4 import QtCore, QtGui
8 import zmq
8 import zmq
9
9
10 # Local imports
10 # Local imports
11 from IPython.core.inputsplitter import InputSplitter
11 from IPython.core.inputsplitter import InputSplitter
12 from call_tip_widget import CallTipWidget
12 from call_tip_widget import CallTipWidget
13 from completion_lexer import CompletionLexer
13 from completion_lexer import CompletionLexer
14 from console_widget import HistoryConsoleWidget
14 from console_widget import HistoryConsoleWidget
15 from pygments_highlighter import PygmentsHighlighter
15 from pygments_highlighter import PygmentsHighlighter
16
16
17
17
18 class FrontendHighlighter(PygmentsHighlighter):
18 class FrontendHighlighter(PygmentsHighlighter):
19 """ A PygmentsHighlighter that can be turned on and off and that ignores
19 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 prompts.
20 prompts.
21 """
21 """
22
22
23 def __init__(self, frontend):
23 def __init__(self, frontend):
24 super(FrontendHighlighter, self).__init__(frontend.document())
24 super(FrontendHighlighter, self).__init__(frontend.document())
25 self._current_offset = 0
25 self._current_offset = 0
26 self._frontend = frontend
26 self._frontend = frontend
27 self.highlighting_on = False
27 self.highlighting_on = False
28
28
29 def highlightBlock(self, qstring):
29 def highlightBlock(self, qstring):
30 """ Highlight a block of text. Reimplemented to highlight selectively.
30 """ Highlight a block of text. Reimplemented to highlight selectively.
31 """
31 """
32 if not self.highlighting_on:
32 if not self.highlighting_on:
33 return
33 return
34
34
35 # The input to this function is unicode string that may contain
35 # The input to this function is unicode string that may contain
36 # paragraph break characters, non-breaking spaces, etc. Here we acquire
36 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 # the string as plain text so we can compare it.
37 # the string as plain text so we can compare it.
38 current_block = self.currentBlock()
38 current_block = self.currentBlock()
39 string = self._frontend._get_block_plain_text(current_block)
39 string = self._frontend._get_block_plain_text(current_block)
40
40
41 # Decide whether to check for the regular or continuation prompt.
41 # Decide whether to check for the regular or continuation prompt.
42 if current_block.contains(self._frontend._prompt_pos):
42 if current_block.contains(self._frontend._prompt_pos):
43 prompt = self._frontend._prompt
43 prompt = self._frontend._prompt
44 else:
44 else:
45 prompt = self._frontend._continuation_prompt
45 prompt = self._frontend._continuation_prompt
46
46
47 # Don't highlight the part of the string that contains the prompt.
47 # Don't highlight the part of the string that contains the prompt.
48 if string.startswith(prompt):
48 if string.startswith(prompt):
49 self._current_offset = len(prompt)
49 self._current_offset = len(prompt)
50 qstring.remove(0, len(prompt))
50 qstring.remove(0, len(prompt))
51 else:
51 else:
52 self._current_offset = 0
52 self._current_offset = 0
53
53
54 PygmentsHighlighter.highlightBlock(self, qstring)
54 PygmentsHighlighter.highlightBlock(self, qstring)
55
55
56 def setFormat(self, start, count, format):
56 def setFormat(self, start, count, format):
57 """ Reimplemented to highlight selectively.
57 """ Reimplemented to highlight selectively.
58 """
58 """
59 start += self._current_offset
59 start += self._current_offset
60 PygmentsHighlighter.setFormat(self, start, count, format)
60 PygmentsHighlighter.setFormat(self, start, count, format)
61
61
62
62
63 class FrontendWidget(HistoryConsoleWidget):
63 class FrontendWidget(HistoryConsoleWidget):
64 """ A Qt frontend for a generic Python kernel.
64 """ A Qt frontend for a generic Python kernel.
65 """
65 """
66
66
67 # Emitted when an 'execute_reply' is received from the kernel.
67 # Emitted when an 'execute_reply' is received from the kernel.
68 executed = QtCore.pyqtSignal(object)
68 executed = QtCore.pyqtSignal(object)
69
69
70 #---------------------------------------------------------------------------
70 #---------------------------------------------------------------------------
71 # 'QObject' interface
71 # 'QObject' interface
72 #---------------------------------------------------------------------------
72 #---------------------------------------------------------------------------
73
73
74 def __init__(self, parent=None):
74 def __init__(self, parent=None):
75 super(FrontendWidget, self).__init__(parent)
75 super(FrontendWidget, self).__init__(parent)
76
76
77 # FrontendWidget protected variables.
77 # FrontendWidget protected variables.
78 self._call_tip_widget = CallTipWidget(self)
78 self._call_tip_widget = CallTipWidget(self)
79 self._completion_lexer = CompletionLexer(PythonLexer())
79 self._completion_lexer = CompletionLexer(PythonLexer())
80 self._hidden = True
80 self._hidden = True
81 self._highlighter = FrontendHighlighter(self)
81 self._highlighter = FrontendHighlighter(self)
82 self._input_splitter = InputSplitter(input_mode='replace')
82 self._input_splitter = InputSplitter(input_mode='replace')
83 self._kernel_manager = None
83 self._kernel_manager = None
84
84
85 # Configure the ConsoleWidget.
85 # Configure the ConsoleWidget.
86 self._set_continuation_prompt('... ')
86 self._set_continuation_prompt('... ')
87
87
88 self.document().contentsChange.connect(self._document_contents_change)
88 self.document().contentsChange.connect(self._document_contents_change)
89
89
90 #---------------------------------------------------------------------------
90 #---------------------------------------------------------------------------
91 # 'QWidget' interface
91 # 'QWidget' interface
92 #---------------------------------------------------------------------------
92 #---------------------------------------------------------------------------
93
93
94 def focusOutEvent(self, event):
94 def focusOutEvent(self, event):
95 """ Reimplemented to hide calltips.
95 """ Reimplemented to hide calltips.
96 """
96 """
97 self._call_tip_widget.hide()
97 self._call_tip_widget.hide()
98 super(FrontendWidget, self).focusOutEvent(event)
98 super(FrontendWidget, self).focusOutEvent(event)
99
99
100 def keyPressEvent(self, event):
100 def keyPressEvent(self, event):
101 """ Reimplemented to allow calltips to process events and to send
101 """ Reimplemented to allow calltips to process events and to send
102 signals to the kernel.
102 signals to the kernel.
103 """
103 """
104 if self._executing and event.key() == QtCore.Qt.Key_C and \
104 if self._executing and event.key() == QtCore.Qt.Key_C and \
105 self._control_down(event.modifiers()):
105 self._control_down(event.modifiers()):
106 self._interrupt_kernel()
106 self._interrupt_kernel()
107 else:
107 else:
108 if self._call_tip_widget.isVisible():
108 if self._call_tip_widget.isVisible():
109 self._call_tip_widget.keyPressEvent(event)
109 self._call_tip_widget.keyPressEvent(event)
110 super(FrontendWidget, self).keyPressEvent(event)
110 super(FrontendWidget, self).keyPressEvent(event)
111
111
112 #---------------------------------------------------------------------------
112 #---------------------------------------------------------------------------
113 # 'ConsoleWidget' abstract interface
113 # 'ConsoleWidget' abstract interface
114 #---------------------------------------------------------------------------
114 #---------------------------------------------------------------------------
115
115
116 def _is_complete(self, source, interactive):
116 def _is_complete(self, source, interactive):
117 """ Returns whether 'source' can be completely processed and a new
117 """ Returns whether 'source' can be completely processed and a new
118 prompt created. When triggered by an Enter/Return key press,
118 prompt created. When triggered by an Enter/Return key press,
119 'interactive' is True; otherwise, it is False.
119 'interactive' is True; otherwise, it is False.
120 """
120 """
121 complete = self._input_splitter.push(source)
121 complete = self._input_splitter.push(source.replace('\t', ' '))
122 if interactive:
122 if interactive:
123 complete = not self._input_splitter.push_accepts_more()
123 complete = not self._input_splitter.push_accepts_more()
124 return complete
124 return complete
125
125
126 def _execute(self, source, hidden):
126 def _execute(self, source, hidden):
127 """ Execute 'source'. If 'hidden', do not show any output.
127 """ Execute 'source'. If 'hidden', do not show any output.
128 """
128 """
129 self.kernel_manager.xreq_channel.execute(source)
129 self.kernel_manager.xreq_channel.execute(source)
130 self._hidden = hidden
130 self._hidden = hidden
131
131
132 def _prompt_started_hook(self):
132 def _prompt_started_hook(self):
133 """ Called immediately after a new prompt is displayed.
133 """ Called immediately after a new prompt is displayed.
134 """
134 """
135 if not self._reading:
135 if not self._reading:
136 self._highlighter.highlighting_on = True
136 self._highlighter.highlighting_on = True
137
137
138 # Auto-indent if this is a continuation prompt.
138 # Auto-indent if this is a continuation prompt.
139 if self._get_prompt_cursor().blockNumber() != \
139 if self._get_prompt_cursor().blockNumber() != \
140 self._get_end_cursor().blockNumber():
140 self._get_end_cursor().blockNumber():
141 self.appendPlainText(' ' * self._input_splitter.indent_spaces)
141 spaces = self._input_splitter.indent_spaces
142 self.appendPlainText('\t' * (spaces / 4) + ' ' * (spaces % 4))
142
143
143 def _prompt_finished_hook(self):
144 def _prompt_finished_hook(self):
144 """ Called immediately after a prompt is finished, i.e. when some input
145 """ Called immediately after a prompt is finished, i.e. when some input
145 will be processed and a new prompt displayed.
146 will be processed and a new prompt displayed.
146 """
147 """
147 if not self._reading:
148 if not self._reading:
148 self._highlighter.highlighting_on = False
149 self._highlighter.highlighting_on = False
149
150
150 def _tab_pressed(self):
151 def _tab_pressed(self):
151 """ Called when the tab key is pressed. Returns whether to continue
152 """ Called when the tab key is pressed. Returns whether to continue
152 processing the event.
153 processing the event.
153 """
154 """
154 self._keep_cursor_in_buffer()
155 self._keep_cursor_in_buffer()
155 cursor = self.textCursor()
156 cursor = self.textCursor()
156 if not self._complete():
157 return not self._complete()
157 cursor.insertText(' ')
158 return False
159
158
160 #---------------------------------------------------------------------------
159 #---------------------------------------------------------------------------
161 # 'FrontendWidget' interface
160 # 'FrontendWidget' interface
162 #---------------------------------------------------------------------------
161 #---------------------------------------------------------------------------
163
162
164 def execute_file(self, path, hidden=False):
163 def execute_file(self, path, hidden=False):
165 """ Attempts to execute file with 'path'. If 'hidden', no output is
164 """ Attempts to execute file with 'path'. If 'hidden', no output is
166 shown.
165 shown.
167 """
166 """
168 self.execute('execfile("%s")' % path, hidden=hidden)
167 self.execute('execfile("%s")' % path, hidden=hidden)
169
168
170 def _get_kernel_manager(self):
169 def _get_kernel_manager(self):
171 """ Returns the current kernel manager.
170 """ Returns the current kernel manager.
172 """
171 """
173 return self._kernel_manager
172 return self._kernel_manager
174
173
175 def _set_kernel_manager(self, kernel_manager):
174 def _set_kernel_manager(self, kernel_manager):
176 """ Disconnect from the current kernel manager (if any) and set a new
175 """ Disconnect from the current kernel manager (if any) and set a new
177 kernel manager.
176 kernel manager.
178 """
177 """
179 # Disconnect the old kernel manager, if necessary.
178 # Disconnect the old kernel manager, if necessary.
180 if self._kernel_manager is not None:
179 if self._kernel_manager is not None:
181 self._kernel_manager.started_channels.disconnect(
180 self._kernel_manager.started_channels.disconnect(
182 self._started_channels)
181 self._started_channels)
183 self._kernel_manager.stopped_channels.disconnect(
182 self._kernel_manager.stopped_channels.disconnect(
184 self._stopped_channels)
183 self._stopped_channels)
185
184
186 # Disconnect the old kernel manager's channels.
185 # Disconnect the old kernel manager's channels.
187 sub = self._kernel_manager.sub_channel
186 sub = self._kernel_manager.sub_channel
188 xreq = self._kernel_manager.xreq_channel
187 xreq = self._kernel_manager.xreq_channel
189 rep = self._kernel_manager.rep_channel
188 rep = self._kernel_manager.rep_channel
190 sub.message_received.disconnect(self._handle_sub)
189 sub.message_received.disconnect(self._handle_sub)
191 xreq.execute_reply.disconnect(self._handle_execute_reply)
190 xreq.execute_reply.disconnect(self._handle_execute_reply)
192 xreq.complete_reply.disconnect(self._handle_complete_reply)
191 xreq.complete_reply.disconnect(self._handle_complete_reply)
193 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
192 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
194 rep.readline_requested.disconnect(self._handle_req)
193 rep.readline_requested.disconnect(self._handle_req)
195
194
196 # Handle the case where the old kernel manager is still listening.
195 # Handle the case where the old kernel manager is still listening.
197 if self._kernel_manager.channels_running:
196 if self._kernel_manager.channels_running:
198 self._stopped_channels()
197 self._stopped_channels()
199
198
200 # Set the new kernel manager.
199 # Set the new kernel manager.
201 self._kernel_manager = kernel_manager
200 self._kernel_manager = kernel_manager
202 if kernel_manager is None:
201 if kernel_manager is None:
203 return
202 return
204
203
205 # Connect the new kernel manager.
204 # Connect the new kernel manager.
206 kernel_manager.started_channels.connect(self._started_channels)
205 kernel_manager.started_channels.connect(self._started_channels)
207 kernel_manager.stopped_channels.connect(self._stopped_channels)
206 kernel_manager.stopped_channels.connect(self._stopped_channels)
208
207
209 # Connect the new kernel manager's channels.
208 # Connect the new kernel manager's channels.
210 sub = kernel_manager.sub_channel
209 sub = kernel_manager.sub_channel
211 xreq = kernel_manager.xreq_channel
210 xreq = kernel_manager.xreq_channel
212 rep = kernel_manager.rep_channel
211 rep = kernel_manager.rep_channel
213 sub.message_received.connect(self._handle_sub)
212 sub.message_received.connect(self._handle_sub)
214 xreq.execute_reply.connect(self._handle_execute_reply)
213 xreq.execute_reply.connect(self._handle_execute_reply)
215 xreq.complete_reply.connect(self._handle_complete_reply)
214 xreq.complete_reply.connect(self._handle_complete_reply)
216 xreq.object_info_reply.connect(self._handle_object_info_reply)
215 xreq.object_info_reply.connect(self._handle_object_info_reply)
217 rep.readline_requested.connect(self._handle_req)
216 rep.readline_requested.connect(self._handle_req)
218
217
219 # Handle the case where the kernel manager started channels before
218 # Handle the case where the kernel manager started channels before
220 # we connected.
219 # we connected.
221 if kernel_manager.channels_running:
220 if kernel_manager.channels_running:
222 self._started_channels()
221 self._started_channels()
223
222
224 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
223 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
225
224
226 #---------------------------------------------------------------------------
225 #---------------------------------------------------------------------------
227 # 'FrontendWidget' protected interface
226 # 'FrontendWidget' protected interface
228 #---------------------------------------------------------------------------
227 #---------------------------------------------------------------------------
229
228
230 def _call_tip(self):
229 def _call_tip(self):
231 """ Shows a call tip, if appropriate, at the current cursor location.
230 """ Shows a call tip, if appropriate, at the current cursor location.
232 """
231 """
233 # Decide if it makes sense to show a call tip
232 # Decide if it makes sense to show a call tip
234 cursor = self.textCursor()
233 cursor = self.textCursor()
235 cursor.movePosition(QtGui.QTextCursor.Left)
234 cursor.movePosition(QtGui.QTextCursor.Left)
236 document = self.document()
235 document = self.document()
237 if document.characterAt(cursor.position()).toAscii() != '(':
236 if document.characterAt(cursor.position()).toAscii() != '(':
238 return False
237 return False
239 context = self._get_context(cursor)
238 context = self._get_context(cursor)
240 if not context:
239 if not context:
241 return False
240 return False
242
241
243 # Send the metadata request to the kernel
242 # Send the metadata request to the kernel
244 name = '.'.join(context)
243 name = '.'.join(context)
245 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
244 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
246 self._calltip_pos = self.textCursor().position()
245 self._calltip_pos = self.textCursor().position()
247 return True
246 return True
248
247
249 def _complete(self):
248 def _complete(self):
250 """ Performs completion at the current cursor location.
249 """ Performs completion at the current cursor location.
251 """
250 """
252 # Decide if it makes sense to do completion
251 # Decide if it makes sense to do completion
253 context = self._get_context()
252 context = self._get_context()
254 if not context:
253 if not context:
255 return False
254 return False
256
255
257 # Send the completion request to the kernel
256 # Send the completion request to the kernel
258 text = '.'.join(context)
257 text = '.'.join(context)
259 self._complete_id = self.kernel_manager.xreq_channel.complete(
258 self._complete_id = self.kernel_manager.xreq_channel.complete(
260 text, self.input_buffer_cursor_line, self.input_buffer)
259 text, self.input_buffer_cursor_line, self.input_buffer)
261 self._complete_pos = self.textCursor().position()
260 self._complete_pos = self.textCursor().position()
262 return True
261 return True
263
262
264 def _get_banner(self):
263 def _get_banner(self):
265 """ Gets a banner to display at the beginning of a session.
264 """ Gets a banner to display at the beginning of a session.
266 """
265 """
267 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
266 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
268 '"license" for more information.'
267 '"license" for more information.'
269 return banner % (sys.version, sys.platform)
268 return banner % (sys.version, sys.platform)
270
269
271 def _get_context(self, cursor=None):
270 def _get_context(self, cursor=None):
272 """ Gets the context at the current cursor location.
271 """ Gets the context at the current cursor location.
273 """
272 """
274 if cursor is None:
273 if cursor is None:
275 cursor = self.textCursor()
274 cursor = self.textCursor()
276 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
275 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
277 QtGui.QTextCursor.KeepAnchor)
276 QtGui.QTextCursor.KeepAnchor)
278 text = unicode(cursor.selectedText())
277 text = unicode(cursor.selectedText())
279 return self._completion_lexer.get_context(text)
278 return self._completion_lexer.get_context(text)
280
279
281 def _interrupt_kernel(self):
280 def _interrupt_kernel(self):
282 """ Attempts to the interrupt the kernel.
281 """ Attempts to the interrupt the kernel.
283 """
282 """
284 if self.kernel_manager.has_kernel:
283 if self.kernel_manager.has_kernel:
285 self.kernel_manager.signal_kernel(signal.SIGINT)
284 self.kernel_manager.signal_kernel(signal.SIGINT)
286 else:
285 else:
287 self.appendPlainText('Kernel process is either remote or '
286 self.appendPlainText('Kernel process is either remote or '
288 'unspecified. Cannot interrupt.\n')
287 'unspecified. Cannot interrupt.\n')
289
288
290 def _show_interpreter_prompt(self):
289 def _show_interpreter_prompt(self):
291 """ Shows a prompt for the interpreter.
290 """ Shows a prompt for the interpreter.
292 """
291 """
293 self._show_prompt('>>> ')
292 self._show_prompt('>>> ')
294
293
295 #------ Signal handlers ----------------------------------------------------
294 #------ Signal handlers ----------------------------------------------------
296
295
297 def _started_channels(self):
296 def _started_channels(self):
298 """ Called when the kernel manager has started listening.
297 """ Called when the kernel manager has started listening.
299 """
298 """
300 self._reset()
299 self._reset()
301 self.appendPlainText(self._get_banner())
300 self.appendPlainText(self._get_banner())
302 self._show_interpreter_prompt()
301 self._show_interpreter_prompt()
303
302
304 def _stopped_channels(self):
303 def _stopped_channels(self):
305 """ Called when the kernel manager has stopped listening.
304 """ Called when the kernel manager has stopped listening.
306 """
305 """
307 # FIXME: Print a message here?
306 # FIXME: Print a message here?
308 pass
307 pass
309
308
310 def _document_contents_change(self, position, removed, added):
309 def _document_contents_change(self, position, removed, added):
311 """ Called whenever the document's content changes. Display a calltip
310 """ Called whenever the document's content changes. Display a calltip
312 if appropriate.
311 if appropriate.
313 """
312 """
314 # Calculate where the cursor should be *after* the change:
313 # Calculate where the cursor should be *after* the change:
315 position += added
314 position += added
316
315
317 document = self.document()
316 document = self.document()
318 if position == self.textCursor().position():
317 if position == self.textCursor().position():
319 self._call_tip()
318 self._call_tip()
320
319
321 def _handle_req(self, req):
320 def _handle_req(self, req):
322 # Make sure that all output from the SUB channel has been processed
321 # Make sure that all output from the SUB channel has been processed
323 # before entering readline mode.
322 # before entering readline mode.
324 self.kernel_manager.sub_channel.flush()
323 self.kernel_manager.sub_channel.flush()
325
324
326 def callback(line):
325 def callback(line):
327 self.kernel_manager.rep_channel.readline(line)
326 self.kernel_manager.rep_channel.readline(line)
328 self._readline(callback=callback)
327 self._readline(callback=callback)
329
328
330 def _handle_sub(self, omsg):
329 def _handle_sub(self, omsg):
331 if self._hidden:
330 if self._hidden:
332 return
331 return
333 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
332 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
334 if handler is not None:
333 if handler is not None:
335 handler(omsg)
334 handler(omsg)
336
335
337 def _handle_pyout(self, omsg):
336 def _handle_pyout(self, omsg):
338 self.appendPlainText(omsg['content']['data'] + '\n')
337 self.appendPlainText(omsg['content']['data'] + '\n')
339
338
340 def _handle_stream(self, omsg):
339 def _handle_stream(self, omsg):
341 self.appendPlainText(omsg['content']['data'])
340 self.appendPlainText(omsg['content']['data'])
342 self.moveCursor(QtGui.QTextCursor.End)
341 self.moveCursor(QtGui.QTextCursor.End)
343
342
344 def _handle_execute_reply(self, rep):
343 def _handle_execute_reply(self, reply):
345 if self._hidden:
344 if self._hidden:
346 return
345 return
347
346
348 # Make sure that all output from the SUB channel has been processed
347 # Make sure that all output from the SUB channel has been processed
349 # before writing a new prompt.
348 # before writing a new prompt.
350 self.kernel_manager.sub_channel.flush()
349 self.kernel_manager.sub_channel.flush()
351
350
352 content = rep['content']
351 status = reply['content']['status']
353 status = content['status']
354 if status == 'error':
352 if status == 'error':
355 self.appendPlainText(content['traceback'][-1])
353 self._handle_execute_error(reply)
356 elif status == 'aborted':
354 elif status == 'aborted':
357 text = "ERROR: ABORTED\n"
355 text = "ERROR: ABORTED\n"
358 self.appendPlainText(text)
356 self.appendPlainText(text)
359 self._hidden = True
357 self._hidden = True
360 self._show_interpreter_prompt()
358 self._show_interpreter_prompt()
361 self.executed.emit(rep)
359 self.executed.emit(reply)
360
361 def _handle_execute_error(self, reply):
362 content = reply['content']
363 traceback = ''.join(content['traceback'])
364 self.appendPlainText(traceback)
362
365
363 def _handle_complete_reply(self, rep):
366 def _handle_complete_reply(self, rep):
364 cursor = self.textCursor()
367 cursor = self.textCursor()
365 if rep['parent_header']['msg_id'] == self._complete_id and \
368 if rep['parent_header']['msg_id'] == self._complete_id and \
366 cursor.position() == self._complete_pos:
369 cursor.position() == self._complete_pos:
367 text = '.'.join(self._get_context())
370 text = '.'.join(self._get_context())
368 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
371 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
369 self._complete_with_items(cursor, rep['content']['matches'])
372 self._complete_with_items(cursor, rep['content']['matches'])
370
373
371 def _handle_object_info_reply(self, rep):
374 def _handle_object_info_reply(self, rep):
372 cursor = self.textCursor()
375 cursor = self.textCursor()
373 if rep['parent_header']['msg_id'] == self._calltip_id and \
376 if rep['parent_header']['msg_id'] == self._calltip_id and \
374 cursor.position() == self._calltip_pos:
377 cursor.position() == self._calltip_pos:
375 doc = rep['content']['docstring']
378 doc = rep['content']['docstring']
376 if doc:
379 if doc:
377 self._call_tip_widget.show_docstring(doc)
380 self._call_tip_widget.show_docstring(doc)
@@ -1,148 +1,164 b''
1 # System library imports
1 # System library imports
2 from PyQt4 import QtCore, QtGui
2 from PyQt4 import QtCore, QtGui
3
3
4 # Local imports
4 # Local imports
5 from IPython.core.usage import default_banner
5 from IPython.core.usage import default_banner
6 from frontend_widget import FrontendWidget
6 from frontend_widget import FrontendWidget
7
7
8
8
9 class IPythonWidget(FrontendWidget):
9 class IPythonWidget(FrontendWidget):
10 """ A FrontendWidget for an IPython kernel.
10 """ A FrontendWidget for an IPython kernel.
11 """
11 """
12
12
13 # The default stylesheet for prompts, colors, etc.
13 # The default stylesheet for prompts, colors, etc.
14 default_stylesheet = """
14 default_stylesheet = """
15 .error { color: red; }
15 .in-prompt { color: navy; }
16 .in-prompt { color: navy; }
16 .in-prompt-number { font-weight: bold; }
17 .in-prompt-number { font-weight: bold; }
17 .out-prompt { color: darkred; }
18 .out-prompt { color: darkred; }
18 .out-prompt-number { font-weight: bold; }
19 .out-prompt-number { font-weight: bold; }
19 """
20 """
20
21
21 #---------------------------------------------------------------------------
22 #---------------------------------------------------------------------------
22 # 'QObject' interface
23 # 'QObject' interface
23 #---------------------------------------------------------------------------
24 #---------------------------------------------------------------------------
24
25
25 def __init__(self, parent=None):
26 def __init__(self, parent=None):
26 super(IPythonWidget, self).__init__(parent)
27 super(IPythonWidget, self).__init__(parent)
27
28
28 # Initialize protected variables.
29 # Initialize protected variables.
29 self._magic_overrides = {}
30 self._magic_overrides = {}
30 self._prompt_count = 0
31 self._prompt_count = 0
31
32
32 # Set a default stylesheet.
33 # Set a default stylesheet.
33 self.set_style_sheet(self.default_stylesheet)
34 self.set_style_sheet(self.default_stylesheet)
34
35
35 #---------------------------------------------------------------------------
36 #---------------------------------------------------------------------------
36 # 'ConsoleWidget' abstract interface
37 # 'ConsoleWidget' abstract interface
37 #---------------------------------------------------------------------------
38 #---------------------------------------------------------------------------
38
39
39 def _execute(self, source, hidden):
40 def _execute(self, source, hidden):
40 """ Reimplemented to override magic commands.
41 """ Reimplemented to override magic commands.
41 """
42 """
42 magic_source = source.strip()
43 magic_source = source.strip()
43 if magic_source.startswith('%'):
44 if magic_source.startswith('%'):
44 magic_source = magic_source[1:]
45 magic_source = magic_source[1:]
45 magic, sep, arguments = magic_source.partition(' ')
46 magic, sep, arguments = magic_source.partition(' ')
46 if not magic:
47 if not magic:
47 magic = magic_source
48 magic = magic_source
48
49
49 callback = self._magic_overrides.get(magic)
50 callback = self._magic_overrides.get(magic)
50 if callback:
51 if callback:
51 output = callback(arguments)
52 output = callback(arguments)
52 if output:
53 if output:
53 self.appendPlainText(output)
54 self.appendPlainText(output)
54 self._show_interpreter_prompt()
55 self._show_interpreter_prompt()
55 else:
56 else:
56 super(IPythonWidget, self)._execute(source, hidden)
57 super(IPythonWidget, self)._execute(source, hidden)
57
58
58 #---------------------------------------------------------------------------
59 #---------------------------------------------------------------------------
59 # 'FrontendWidget' interface
60 # 'FrontendWidget' interface
60 #---------------------------------------------------------------------------
61 #---------------------------------------------------------------------------
61
62
62 def execute_file(self, path, hidden=False):
63 def execute_file(self, path, hidden=False):
63 """ Reimplemented to use the 'run' magic.
64 """ Reimplemented to use the 'run' magic.
64 """
65 """
65 self.execute('run %s' % path, hidden=hidden)
66 self.execute('run %s' % path, hidden=hidden)
66
67
67 #---------------------------------------------------------------------------
68 #---------------------------------------------------------------------------
68 # 'FrontendWidget' protected interface
69 # 'FrontendWidget' protected interface
69 #---------------------------------------------------------------------------
70 #---------------------------------------------------------------------------
70
71
71 def _get_banner(self):
72 def _get_banner(self):
72 """ Reimplemented to return IPython's default banner.
73 """ Reimplemented to return IPython's default banner.
73 """
74 """
74 return default_banner
75 return default_banner
75
76
76 def _show_interpreter_prompt(self):
77 def _show_interpreter_prompt(self):
77 """ Reimplemented for IPython-style prompts.
78 """ Reimplemented for IPython-style prompts.
78 """
79 """
79 self._prompt_count += 1
80 self._prompt_count += 1
80 prompt_template = '<span class="in-prompt">%s</span>'
81 prompt_template = '<span class="in-prompt">%s</span>'
81 prompt_body = '<br/>In [<span class="in-prompt-number">%i</span>]: '
82 prompt_body = '<br/>In [<span class="in-prompt-number">%i</span>]: '
82 prompt = (prompt_template % prompt_body) % self._prompt_count
83 prompt = (prompt_template % prompt_body) % self._prompt_count
83 self._show_prompt(prompt, html=True)
84 self._show_prompt(prompt, html=True)
84
85
85 # Update continuation prompt to reflect (possibly) new prompt length.
86 # Update continuation prompt to reflect (possibly) new prompt length.
86 cont_prompt_chars = '...: '
87 cont_prompt_chars = '...: '
87 space_count = len(self._prompt.lstrip()) - len(cont_prompt_chars)
88 space_count = len(self._prompt.lstrip()) - len(cont_prompt_chars)
88 cont_prompt_body = '&nbsp;' * space_count + cont_prompt_chars
89 cont_prompt_body = '&nbsp;' * space_count + cont_prompt_chars
89 self._continuation_prompt_html = prompt_template % cont_prompt_body
90 self._continuation_prompt_html = prompt_template % cont_prompt_body
90
91
91 #------ Signal handlers ----------------------------------------------------
92 #------ Signal handlers ----------------------------------------------------
92
93
94 def _handle_execute_error(self, reply):
95 """ Reimplemented for IPython-style traceback formatting.
96 """
97 content = reply['content']
98 traceback_lines = content['traceback'][:]
99 traceback = ''.join(traceback_lines)
100 traceback = traceback.replace(' ', '&nbsp;')
101 traceback = traceback.replace('\n', '<br/>')
102
103 ename = content['ename']
104 ename_styled = '<span class="error">%s</span>' % ename
105 traceback = traceback.replace(ename, ename_styled)
106
107 self.appendHtml(traceback)
108
93 def _handle_pyout(self, omsg):
109 def _handle_pyout(self, omsg):
94 """ Reimplemented for IPython-style "display hook".
110 """ Reimplemented for IPython-style "display hook".
95 """
111 """
96 prompt_template = '<span class="out-prompt">%s</span>'
112 prompt_template = '<span class="out-prompt">%s</span>'
97 prompt_body = 'Out[<span class="out-prompt-number">%i</span>]: '
113 prompt_body = 'Out[<span class="out-prompt-number">%i</span>]: '
98 prompt = (prompt_template % prompt_body) % self._prompt_count
114 prompt = (prompt_template % prompt_body) % self._prompt_count
99 self.appendHtml(prompt)
115 self.appendHtml(prompt)
100 self.appendPlainText(omsg['content']['data'] + '\n')
116 self.appendPlainText(omsg['content']['data'] + '\n')
101
117
102 #---------------------------------------------------------------------------
118 #---------------------------------------------------------------------------
103 # 'IPythonWidget' interface
119 # 'IPythonWidget' interface
104 #---------------------------------------------------------------------------
120 #---------------------------------------------------------------------------
105
121
106 def set_magic_override(self, magic, callback):
122 def set_magic_override(self, magic, callback):
107 """ Overrides an IPython magic command. This magic will be intercepted
123 """ Overrides an IPython magic command. This magic will be intercepted
108 by the frontend rather than passed on to the kernel and 'callback'
124 by the frontend rather than passed on to the kernel and 'callback'
109 will be called with a single argument: a string of argument(s) for
125 will be called with a single argument: a string of argument(s) for
110 the magic. The callback can (optionally) return text to print to the
126 the magic. The callback can (optionally) return text to print to the
111 console.
127 console.
112 """
128 """
113 self._magic_overrides[magic] = callback
129 self._magic_overrides[magic] = callback
114
130
115 def remove_magic_override(self, magic):
131 def remove_magic_override(self, magic):
116 """ Removes the override for the specified magic, if there is one.
132 """ Removes the override for the specified magic, if there is one.
117 """
133 """
118 try:
134 try:
119 del self._magic_overrides[magic]
135 del self._magic_overrides[magic]
120 except KeyError:
136 except KeyError:
121 pass
137 pass
122
138
123 def set_style_sheet(self, stylesheet):
139 def set_style_sheet(self, stylesheet):
124 """ Sets the style sheet.
140 """ Sets the style sheet.
125 """
141 """
126 self.document().setDefaultStyleSheet(stylesheet)
142 self.document().setDefaultStyleSheet(stylesheet)
127
143
128
144
129 if __name__ == '__main__':
145 if __name__ == '__main__':
130 from IPython.frontend.qt.kernelmanager import QtKernelManager
146 from IPython.frontend.qt.kernelmanager import QtKernelManager
131
147
132 # Don't let Qt or ZMQ swallow KeyboardInterupts.
148 # Don't let Qt or ZMQ swallow KeyboardInterupts.
133 import signal
149 import signal
134 signal.signal(signal.SIGINT, signal.SIG_DFL)
150 signal.signal(signal.SIGINT, signal.SIG_DFL)
135
151
136 # Create a KernelManager.
152 # Create a KernelManager.
137 kernel_manager = QtKernelManager()
153 kernel_manager = QtKernelManager()
138 kernel_manager.start_kernel()
154 kernel_manager.start_kernel()
139 kernel_manager.start_channels()
155 kernel_manager.start_channels()
140
156
141 # Launch the application.
157 # Launch the application.
142 app = QtGui.QApplication([])
158 app = QtGui.QApplication([])
143 widget = IPythonWidget()
159 widget = IPythonWidget()
144 widget.kernel_manager = kernel_manager
160 widget.kernel_manager = kernel_manager
145 widget.setWindowTitle('Python')
161 widget.setWindowTitle('Python')
146 widget.resize(640, 480)
162 widget.resize(640, 480)
147 widget.show()
163 widget.show()
148 app.exec_()
164 app.exec_()
@@ -1,535 +1,535 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 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 # Standard library imports.
18 # Standard library imports.
19 import __builtin__
19 import __builtin__
20 from code import CommandCompiler
20 from code import CommandCompiler
21 import os
21 import os
22 import sys
22 import sys
23 from threading import Thread
23 from threading import Thread
24 import time
24 import time
25 import traceback
25 import traceback
26
26
27 # System library imports.
27 # System library imports.
28 import zmq
28 import zmq
29
29
30 # Local imports.
30 # Local imports.
31 from IPython.external.argparse import ArgumentParser
31 from IPython.external.argparse import ArgumentParser
32 from session import Session, Message, extract_header
32 from session import Session, Message, extract_header
33 from completer import KernelCompleter
33 from completer import KernelCompleter
34
34
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36 # Kernel and stream classes
36 # Kernel and stream classes
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38
38
39 class InStream(object):
39 class InStream(object):
40 """ A file like object that reads from a 0MQ XREQ socket."""
40 """ A file like object that reads from a 0MQ XREQ socket."""
41
41
42 def __init__(self, session, socket):
42 def __init__(self, session, socket):
43 self.session = session
43 self.session = session
44 self.socket = socket
44 self.socket = socket
45
45
46 def close(self):
46 def close(self):
47 self.socket = None
47 self.socket = None
48
48
49 def flush(self):
49 def flush(self):
50 if self.socket is None:
50 if self.socket is None:
51 raise ValueError('I/O operation on closed file')
51 raise ValueError('I/O operation on closed file')
52
52
53 def isatty(self):
53 def isatty(self):
54 return False
54 return False
55
55
56 def next(self):
56 def next(self):
57 raise IOError('Seek not supported.')
57 raise IOError('Seek not supported.')
58
58
59 def read(self, size=-1):
59 def read(self, size=-1):
60 # FIXME: Do we want another request for this?
60 # FIXME: Do we want another request for this?
61 string = '\n'.join(self.readlines())
61 string = '\n'.join(self.readlines())
62 return self._truncate(string, size)
62 return self._truncate(string, size)
63
63
64 def readline(self, size=-1):
64 def readline(self, size=-1):
65 if self.socket is None:
65 if self.socket is None:
66 raise ValueError('I/O operation on closed file')
66 raise ValueError('I/O operation on closed file')
67 else:
67 else:
68 content = dict(size=size)
68 content = dict(size=size)
69 msg = self.session.msg('readline_request', content=content)
69 msg = self.session.msg('readline_request', content=content)
70 reply = self._request(msg)
70 reply = self._request(msg)
71 line = reply['content']['line']
71 line = reply['content']['line']
72 return self._truncate(line, size)
72 return self._truncate(line, size)
73
73
74 def readlines(self, sizehint=-1):
74 def readlines(self, sizehint=-1):
75 # Sizehint is ignored, as is permitted.
75 # Sizehint is ignored, as is permitted.
76 if self.socket is None:
76 if self.socket is None:
77 raise ValueError('I/O operation on closed file')
77 raise ValueError('I/O operation on closed file')
78 else:
78 else:
79 lines = []
79 lines = []
80 while True:
80 while True:
81 line = self.readline()
81 line = self.readline()
82 if line:
82 if line:
83 lines.append(line)
83 lines.append(line)
84 else:
84 else:
85 break
85 break
86 return lines
86 return lines
87
87
88 def seek(self, offset, whence=None):
88 def seek(self, offset, whence=None):
89 raise IOError('Seek not supported.')
89 raise IOError('Seek not supported.')
90
90
91 def write(self, string):
91 def write(self, string):
92 raise IOError('Write not supported on a read only stream.')
92 raise IOError('Write not supported on a read only stream.')
93
93
94 def writelines(self, sequence):
94 def writelines(self, sequence):
95 raise IOError('Write not supported on a read only stream.')
95 raise IOError('Write not supported on a read only stream.')
96
96
97 def _request(self, msg):
97 def _request(self, msg):
98 # Flush output before making the request. This ensures, for example,
98 # Flush output before making the request. This ensures, for example,
99 # that raw_input(prompt) actually gets a prompt written.
99 # that raw_input(prompt) actually gets a prompt written.
100 sys.stderr.flush()
100 sys.stderr.flush()
101 sys.stdout.flush()
101 sys.stdout.flush()
102
102
103 self.socket.send_json(msg)
103 self.socket.send_json(msg)
104 while True:
104 while True:
105 try:
105 try:
106 reply = self.socket.recv_json(zmq.NOBLOCK)
106 reply = self.socket.recv_json(zmq.NOBLOCK)
107 except zmq.ZMQError, e:
107 except zmq.ZMQError, e:
108 if e.errno == zmq.EAGAIN:
108 if e.errno == zmq.EAGAIN:
109 pass
109 pass
110 else:
110 else:
111 raise
111 raise
112 else:
112 else:
113 break
113 break
114 return reply
114 return reply
115
115
116 def _truncate(self, string, size):
116 def _truncate(self, string, size):
117 if size >= 0:
117 if size >= 0:
118 if isinstance(string, str):
118 if isinstance(string, str):
119 return string[:size]
119 return string[:size]
120 elif isinstance(string, unicode):
120 elif isinstance(string, unicode):
121 encoded = string.encode('utf-8')[:size]
121 encoded = string.encode('utf-8')[:size]
122 return encoded.decode('utf-8', 'ignore')
122 return encoded.decode('utf-8', 'ignore')
123 return string
123 return string
124
124
125
125
126 class OutStream(object):
126 class OutStream(object):
127 """A file like object that publishes the stream to a 0MQ PUB socket."""
127 """A file like object that publishes the stream to a 0MQ PUB socket."""
128
128
129 def __init__(self, session, pub_socket, name, max_buffer=200):
129 def __init__(self, session, pub_socket, name, max_buffer=200):
130 self.session = session
130 self.session = session
131 self.pub_socket = pub_socket
131 self.pub_socket = pub_socket
132 self.name = name
132 self.name = name
133 self._buffer = []
133 self._buffer = []
134 self._buffer_len = 0
134 self._buffer_len = 0
135 self.max_buffer = max_buffer
135 self.max_buffer = max_buffer
136 self.parent_header = {}
136 self.parent_header = {}
137
137
138 def set_parent(self, parent):
138 def set_parent(self, parent):
139 self.parent_header = extract_header(parent)
139 self.parent_header = extract_header(parent)
140
140
141 def close(self):
141 def close(self):
142 self.pub_socket = None
142 self.pub_socket = None
143
143
144 def flush(self):
144 def flush(self):
145 if self.pub_socket is None:
145 if self.pub_socket is None:
146 raise ValueError(u'I/O operation on closed file')
146 raise ValueError(u'I/O operation on closed file')
147 else:
147 else:
148 if self._buffer:
148 if self._buffer:
149 data = ''.join(self._buffer)
149 data = ''.join(self._buffer)
150 content = {u'name':self.name, u'data':data}
150 content = {u'name':self.name, u'data':data}
151 msg = self.session.msg(u'stream', content=content,
151 msg = self.session.msg(u'stream', content=content,
152 parent=self.parent_header)
152 parent=self.parent_header)
153 print>>sys.__stdout__, Message(msg)
153 print>>sys.__stdout__, Message(msg)
154 self.pub_socket.send_json(msg)
154 self.pub_socket.send_json(msg)
155 self._buffer_len = 0
155 self._buffer_len = 0
156 self._buffer = []
156 self._buffer = []
157
157
158 def isatty(self):
158 def isatty(self):
159 return False
159 return False
160
160
161 def next(self):
161 def next(self):
162 raise IOError('Read not supported on a write only stream.')
162 raise IOError('Read not supported on a write only stream.')
163
163
164 def read(self, size=None):
164 def read(self, size=None):
165 raise IOError('Read not supported on a write only stream.')
165 raise IOError('Read not supported on a write only stream.')
166
166
167 readline=read
167 readline=read
168
168
169 def write(self, s):
169 def write(self, s):
170 if self.pub_socket is None:
170 if self.pub_socket is None:
171 raise ValueError('I/O operation on closed file')
171 raise ValueError('I/O operation on closed file')
172 else:
172 else:
173 self._buffer.append(s)
173 self._buffer.append(s)
174 self._buffer_len += len(s)
174 self._buffer_len += len(s)
175 self._maybe_send()
175 self._maybe_send()
176
176
177 def _maybe_send(self):
177 def _maybe_send(self):
178 if '\n' in self._buffer[-1]:
178 if '\n' in self._buffer[-1]:
179 self.flush()
179 self.flush()
180 if self._buffer_len > self.max_buffer:
180 if self._buffer_len > self.max_buffer:
181 self.flush()
181 self.flush()
182
182
183 def writelines(self, sequence):
183 def writelines(self, sequence):
184 if self.pub_socket is None:
184 if self.pub_socket is None:
185 raise ValueError('I/O operation on closed file')
185 raise ValueError('I/O operation on closed file')
186 else:
186 else:
187 for s in sequence:
187 for s in sequence:
188 self.write(s)
188 self.write(s)
189
189
190
190
191 class DisplayHook(object):
191 class DisplayHook(object):
192
192
193 def __init__(self, session, pub_socket):
193 def __init__(self, session, pub_socket):
194 self.session = session
194 self.session = session
195 self.pub_socket = pub_socket
195 self.pub_socket = pub_socket
196 self.parent_header = {}
196 self.parent_header = {}
197
197
198 def __call__(self, obj):
198 def __call__(self, obj):
199 if obj is None:
199 if obj is None:
200 return
200 return
201
201
202 __builtin__._ = obj
202 __builtin__._ = obj
203 msg = self.session.msg(u'pyout', {u'data':repr(obj)},
203 msg = self.session.msg(u'pyout', {u'data':repr(obj)},
204 parent=self.parent_header)
204 parent=self.parent_header)
205 self.pub_socket.send_json(msg)
205 self.pub_socket.send_json(msg)
206
206
207 def set_parent(self, parent):
207 def set_parent(self, parent):
208 self.parent_header = extract_header(parent)
208 self.parent_header = extract_header(parent)
209
209
210
210
211 class Kernel(object):
211 class Kernel(object):
212
212
213 def __init__(self, session, reply_socket, pub_socket):
213 def __init__(self, session, reply_socket, pub_socket):
214 self.session = session
214 self.session = session
215 self.reply_socket = reply_socket
215 self.reply_socket = reply_socket
216 self.pub_socket = pub_socket
216 self.pub_socket = pub_socket
217 self.user_ns = {}
217 self.user_ns = {}
218 self.history = []
218 self.history = []
219 self.compiler = CommandCompiler()
219 self.compiler = CommandCompiler()
220 self.completer = KernelCompleter(self.user_ns)
220 self.completer = KernelCompleter(self.user_ns)
221
221
222 # Build dict of handlers for message types
222 # Build dict of handlers for message types
223 msg_types = [ 'execute_request', 'complete_request',
223 msg_types = [ 'execute_request', 'complete_request',
224 'object_info_request' ]
224 'object_info_request' ]
225 self.handlers = {}
225 self.handlers = {}
226 for msg_type in msg_types:
226 for msg_type in msg_types:
227 self.handlers[msg_type] = getattr(self, msg_type)
227 self.handlers[msg_type] = getattr(self, msg_type)
228
228
229 def abort_queue(self):
229 def abort_queue(self):
230 while True:
230 while True:
231 try:
231 try:
232 ident = self.reply_socket.recv(zmq.NOBLOCK)
232 ident = self.reply_socket.recv(zmq.NOBLOCK)
233 except zmq.ZMQError, e:
233 except zmq.ZMQError, e:
234 if e.errno == zmq.EAGAIN:
234 if e.errno == zmq.EAGAIN:
235 break
235 break
236 else:
236 else:
237 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
237 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
238 msg = self.reply_socket.recv_json()
238 msg = self.reply_socket.recv_json()
239 print>>sys.__stdout__, "Aborting:"
239 print>>sys.__stdout__, "Aborting:"
240 print>>sys.__stdout__, Message(msg)
240 print>>sys.__stdout__, Message(msg)
241 msg_type = msg['msg_type']
241 msg_type = msg['msg_type']
242 reply_type = msg_type.split('_')[0] + '_reply'
242 reply_type = msg_type.split('_')[0] + '_reply'
243 reply_msg = self.session.msg(reply_type, {'status' : 'aborted'}, msg)
243 reply_msg = self.session.msg(reply_type, {'status' : 'aborted'}, msg)
244 print>>sys.__stdout__, Message(reply_msg)
244 print>>sys.__stdout__, Message(reply_msg)
245 self.reply_socket.send(ident,zmq.SNDMORE)
245 self.reply_socket.send(ident,zmq.SNDMORE)
246 self.reply_socket.send_json(reply_msg)
246 self.reply_socket.send_json(reply_msg)
247 # We need to wait a bit for requests to come in. This can probably
247 # We need to wait a bit for requests to come in. This can probably
248 # be set shorter for true asynchronous clients.
248 # be set shorter for true asynchronous clients.
249 time.sleep(0.1)
249 time.sleep(0.1)
250
250
251 def execute_request(self, ident, parent):
251 def execute_request(self, ident, parent):
252 try:
252 try:
253 code = parent[u'content'][u'code']
253 code = parent[u'content'][u'code']
254 except:
254 except:
255 print>>sys.__stderr__, "Got bad msg: "
255 print>>sys.__stderr__, "Got bad msg: "
256 print>>sys.__stderr__, Message(parent)
256 print>>sys.__stderr__, Message(parent)
257 return
257 return
258 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
258 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
259 self.pub_socket.send_json(pyin_msg)
259 self.pub_socket.send_json(pyin_msg)
260 try:
260 try:
261 comp_code = self.compiler(code, '<zmq-kernel>')
261 comp_code = self.compiler(code, '<zmq-kernel>')
262 sys.displayhook.set_parent(parent)
262 sys.displayhook.set_parent(parent)
263 exec comp_code in self.user_ns, self.user_ns
263 exec comp_code in self.user_ns, self.user_ns
264 except:
264 except:
265 result = u'error'
265 result = u'error'
266 etype, evalue, tb = sys.exc_info()
266 etype, evalue, tb = sys.exc_info()
267 tb = traceback.format_exception(etype, evalue, tb)
267 tb = traceback.format_exception(etype, evalue, tb)
268 exc_content = {
268 exc_content = {
269 u'status' : u'error',
269 u'status' : u'error',
270 u'traceback' : tb,
270 u'traceback' : tb,
271 u'etype' : unicode(etype),
271 u'ename' : unicode(etype.__name__),
272 u'evalue' : unicode(evalue)
272 u'evalue' : unicode(evalue)
273 }
273 }
274 exc_msg = self.session.msg(u'pyerr', exc_content, parent)
274 exc_msg = self.session.msg(u'pyerr', exc_content, parent)
275 self.pub_socket.send_json(exc_msg)
275 self.pub_socket.send_json(exc_msg)
276 reply_content = exc_content
276 reply_content = exc_content
277 else:
277 else:
278 reply_content = {'status' : 'ok'}
278 reply_content = {'status' : 'ok'}
279 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
279 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
280 print>>sys.__stdout__, Message(reply_msg)
280 print>>sys.__stdout__, Message(reply_msg)
281 self.reply_socket.send(ident, zmq.SNDMORE)
281 self.reply_socket.send(ident, zmq.SNDMORE)
282 self.reply_socket.send_json(reply_msg)
282 self.reply_socket.send_json(reply_msg)
283 if reply_msg['content']['status'] == u'error':
283 if reply_msg['content']['status'] == u'error':
284 self.abort_queue()
284 self.abort_queue()
285
285
286 def complete_request(self, ident, parent):
286 def complete_request(self, ident, parent):
287 matches = {'matches' : self.complete(parent),
287 matches = {'matches' : self.complete(parent),
288 'status' : 'ok'}
288 'status' : 'ok'}
289 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
289 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
290 matches, parent, ident)
290 matches, parent, ident)
291 print >> sys.__stdout__, completion_msg
291 print >> sys.__stdout__, completion_msg
292
292
293 def complete(self, msg):
293 def complete(self, msg):
294 return self.completer.complete(msg.content.line, msg.content.text)
294 return self.completer.complete(msg.content.line, msg.content.text)
295
295
296 def object_info_request(self, ident, parent):
296 def object_info_request(self, ident, parent):
297 context = parent['content']['oname'].split('.')
297 context = parent['content']['oname'].split('.')
298 object_info = self.object_info(context)
298 object_info = self.object_info(context)
299 msg = self.session.send(self.reply_socket, 'object_info_reply',
299 msg = self.session.send(self.reply_socket, 'object_info_reply',
300 object_info, parent, ident)
300 object_info, parent, ident)
301 print >> sys.__stdout__, msg
301 print >> sys.__stdout__, msg
302
302
303 def object_info(self, context):
303 def object_info(self, context):
304 symbol, leftover = self.symbol_from_context(context)
304 symbol, leftover = self.symbol_from_context(context)
305 if symbol is not None and not leftover:
305 if symbol is not None and not leftover:
306 doc = getattr(symbol, '__doc__', '')
306 doc = getattr(symbol, '__doc__', '')
307 else:
307 else:
308 doc = ''
308 doc = ''
309 object_info = dict(docstring = doc)
309 object_info = dict(docstring = doc)
310 return object_info
310 return object_info
311
311
312 def symbol_from_context(self, context):
312 def symbol_from_context(self, context):
313 if not context:
313 if not context:
314 return None, context
314 return None, context
315
315
316 base_symbol_string = context[0]
316 base_symbol_string = context[0]
317 symbol = self.user_ns.get(base_symbol_string, None)
317 symbol = self.user_ns.get(base_symbol_string, None)
318 if symbol is None:
318 if symbol is None:
319 symbol = __builtin__.__dict__.get(base_symbol_string, None)
319 symbol = __builtin__.__dict__.get(base_symbol_string, None)
320 if symbol is None:
320 if symbol is None:
321 return None, context
321 return None, context
322
322
323 context = context[1:]
323 context = context[1:]
324 for i, name in enumerate(context):
324 for i, name in enumerate(context):
325 new_symbol = getattr(symbol, name, None)
325 new_symbol = getattr(symbol, name, None)
326 if new_symbol is None:
326 if new_symbol is None:
327 return symbol, context[i:]
327 return symbol, context[i:]
328 else:
328 else:
329 symbol = new_symbol
329 symbol = new_symbol
330
330
331 return symbol, []
331 return symbol, []
332
332
333 def start(self):
333 def start(self):
334 while True:
334 while True:
335 ident = self.reply_socket.recv()
335 ident = self.reply_socket.recv()
336 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
336 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
337 msg = self.reply_socket.recv_json()
337 msg = self.reply_socket.recv_json()
338 omsg = Message(msg)
338 omsg = Message(msg)
339 print>>sys.__stdout__
339 print>>sys.__stdout__
340 print>>sys.__stdout__, omsg
340 print>>sys.__stdout__, omsg
341 handler = self.handlers.get(omsg.msg_type, None)
341 handler = self.handlers.get(omsg.msg_type, None)
342 if handler is None:
342 if handler is None:
343 print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg
343 print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg
344 else:
344 else:
345 handler(ident, omsg)
345 handler(ident, omsg)
346
346
347 #-----------------------------------------------------------------------------
347 #-----------------------------------------------------------------------------
348 # Kernel main and launch functions
348 # Kernel main and launch functions
349 #-----------------------------------------------------------------------------
349 #-----------------------------------------------------------------------------
350
350
351 class ExitPollerUnix(Thread):
351 class ExitPollerUnix(Thread):
352 """ A Unix-specific daemon thread that terminates the program immediately
352 """ A Unix-specific daemon thread that terminates the program immediately
353 when this process' parent process no longer exists.
353 when this process' parent process no longer exists.
354 """
354 """
355
355
356 def __init__(self):
356 def __init__(self):
357 super(ExitPollerUnix, self).__init__()
357 super(ExitPollerUnix, self).__init__()
358 self.daemon = True
358 self.daemon = True
359
359
360 def run(self):
360 def run(self):
361 # We cannot use os.waitpid because it works only for child processes.
361 # We cannot use os.waitpid because it works only for child processes.
362 from errno import EINTR
362 from errno import EINTR
363 while True:
363 while True:
364 try:
364 try:
365 if os.getppid() == 1:
365 if os.getppid() == 1:
366 os._exit(1)
366 os._exit(1)
367 time.sleep(1.0)
367 time.sleep(1.0)
368 except OSError, e:
368 except OSError, e:
369 if e.errno == EINTR:
369 if e.errno == EINTR:
370 continue
370 continue
371 raise
371 raise
372
372
373 class ExitPollerWindows(Thread):
373 class ExitPollerWindows(Thread):
374 """ A Windows-specific daemon thread that terminates the program immediately
374 """ A Windows-specific daemon thread that terminates the program immediately
375 when a Win32 handle is signaled.
375 when a Win32 handle is signaled.
376 """
376 """
377
377
378 def __init__(self, handle):
378 def __init__(self, handle):
379 super(ExitPollerWindows, self).__init__()
379 super(ExitPollerWindows, self).__init__()
380 self.daemon = True
380 self.daemon = True
381 self.handle = handle
381 self.handle = handle
382
382
383 def run(self):
383 def run(self):
384 from _subprocess import WaitForSingleObject, WAIT_OBJECT_0, INFINITE
384 from _subprocess import WaitForSingleObject, WAIT_OBJECT_0, INFINITE
385 result = WaitForSingleObject(self.handle, INFINITE)
385 result = WaitForSingleObject(self.handle, INFINITE)
386 if result == WAIT_OBJECT_0:
386 if result == WAIT_OBJECT_0:
387 os._exit(1)
387 os._exit(1)
388
388
389
389
390 def bind_port(socket, ip, port):
390 def bind_port(socket, ip, port):
391 """ Binds the specified ZMQ socket. If the port is less than zero, a random
391 """ Binds the specified ZMQ socket. If the port is less than zero, a random
392 port is chosen. Returns the port that was bound.
392 port is chosen. Returns the port that was bound.
393 """
393 """
394 connection = 'tcp://%s' % ip
394 connection = 'tcp://%s' % ip
395 if port <= 0:
395 if port <= 0:
396 port = socket.bind_to_random_port(connection)
396 port = socket.bind_to_random_port(connection)
397 else:
397 else:
398 connection += ':%i' % port
398 connection += ':%i' % port
399 socket.bind(connection)
399 socket.bind(connection)
400 return port
400 return port
401
401
402
402
403 def main():
403 def main():
404 """ Main entry point for launching a kernel.
404 """ Main entry point for launching a kernel.
405 """
405 """
406 # Parse command line arguments.
406 # Parse command line arguments.
407 parser = ArgumentParser()
407 parser = ArgumentParser()
408 parser.add_argument('--ip', type=str, default='127.0.0.1',
408 parser.add_argument('--ip', type=str, default='127.0.0.1',
409 help='set the kernel\'s IP address [default: local]')
409 help='set the kernel\'s IP address [default: local]')
410 parser.add_argument('--xrep', type=int, metavar='PORT', default=0,
410 parser.add_argument('--xrep', type=int, metavar='PORT', default=0,
411 help='set the XREP channel port [default: random]')
411 help='set the XREP channel port [default: random]')
412 parser.add_argument('--pub', type=int, metavar='PORT', default=0,
412 parser.add_argument('--pub', type=int, metavar='PORT', default=0,
413 help='set the PUB channel port [default: random]')
413 help='set the PUB channel port [default: random]')
414 parser.add_argument('--req', type=int, metavar='PORT', default=0,
414 parser.add_argument('--req', type=int, metavar='PORT', default=0,
415 help='set the REQ channel port [default: random]')
415 help='set the REQ channel port [default: random]')
416 if sys.platform == 'win32':
416 if sys.platform == 'win32':
417 parser.add_argument('--parent', type=int, metavar='HANDLE',
417 parser.add_argument('--parent', type=int, metavar='HANDLE',
418 default=0, help='kill this process if the process '
418 default=0, help='kill this process if the process '
419 'with HANDLE dies')
419 'with HANDLE dies')
420 else:
420 else:
421 parser.add_argument('--parent', action='store_true',
421 parser.add_argument('--parent', action='store_true',
422 help='kill this process if its parent dies')
422 help='kill this process if its parent dies')
423 namespace = parser.parse_args()
423 namespace = parser.parse_args()
424
424
425 # Create a context, a session, and the kernel sockets.
425 # Create a context, a session, and the kernel sockets.
426 print >>sys.__stdout__, "Starting the kernel..."
426 print >>sys.__stdout__, "Starting the kernel..."
427 context = zmq.Context()
427 context = zmq.Context()
428 session = Session(username=u'kernel')
428 session = Session(username=u'kernel')
429
429
430 reply_socket = context.socket(zmq.XREP)
430 reply_socket = context.socket(zmq.XREP)
431 xrep_port = bind_port(reply_socket, namespace.ip, namespace.xrep)
431 xrep_port = bind_port(reply_socket, namespace.ip, namespace.xrep)
432 print >>sys.__stdout__, "XREP Channel on port", xrep_port
432 print >>sys.__stdout__, "XREP Channel on port", xrep_port
433
433
434 pub_socket = context.socket(zmq.PUB)
434 pub_socket = context.socket(zmq.PUB)
435 pub_port = bind_port(pub_socket, namespace.ip, namespace.pub)
435 pub_port = bind_port(pub_socket, namespace.ip, namespace.pub)
436 print >>sys.__stdout__, "PUB Channel on port", pub_port
436 print >>sys.__stdout__, "PUB Channel on port", pub_port
437
437
438 req_socket = context.socket(zmq.XREQ)
438 req_socket = context.socket(zmq.XREQ)
439 req_port = bind_port(req_socket, namespace.ip, namespace.req)
439 req_port = bind_port(req_socket, namespace.ip, namespace.req)
440 print >>sys.__stdout__, "REQ Channel on port", req_port
440 print >>sys.__stdout__, "REQ Channel on port", req_port
441
441
442 # Redirect input streams and set a display hook.
442 # Redirect input streams and set a display hook.
443 sys.stdin = InStream(session, req_socket)
443 sys.stdin = InStream(session, req_socket)
444 sys.stdout = OutStream(session, pub_socket, u'stdout')
444 sys.stdout = OutStream(session, pub_socket, u'stdout')
445 sys.stderr = OutStream(session, pub_socket, u'stderr')
445 sys.stderr = OutStream(session, pub_socket, u'stderr')
446 sys.displayhook = DisplayHook(session, pub_socket)
446 sys.displayhook = DisplayHook(session, pub_socket)
447
447
448 # Create the kernel.
448 # Create the kernel.
449 kernel = Kernel(session, reply_socket, pub_socket)
449 kernel = Kernel(session, reply_socket, pub_socket)
450
450
451 # Configure this kernel/process to die on parent termination, if necessary.
451 # Configure this kernel/process to die on parent termination, if necessary.
452 if namespace.parent:
452 if namespace.parent:
453 if sys.platform == 'win32':
453 if sys.platform == 'win32':
454 poller = ExitPollerWindows(namespace.parent)
454 poller = ExitPollerWindows(namespace.parent)
455 else:
455 else:
456 poller = ExitPollerUnix()
456 poller = ExitPollerUnix()
457 poller.start()
457 poller.start()
458
458
459 # Start the kernel mainloop.
459 # Start the kernel mainloop.
460 kernel.start()
460 kernel.start()
461
461
462
462
463 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, independent=False):
463 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, independent=False):
464 """ Launches a localhost kernel, binding to the specified ports.
464 """ Launches a localhost kernel, binding to the specified ports.
465
465
466 Parameters
466 Parameters
467 ----------
467 ----------
468 xrep_port : int, optional
468 xrep_port : int, optional
469 The port to use for XREP channel.
469 The port to use for XREP channel.
470
470
471 pub_port : int, optional
471 pub_port : int, optional
472 The port to use for the SUB channel.
472 The port to use for the SUB channel.
473
473
474 req_port : int, optional
474 req_port : int, optional
475 The port to use for the REQ (raw input) channel.
475 The port to use for the REQ (raw input) channel.
476
476
477 independent : bool, optional (default False)
477 independent : bool, optional (default False)
478 If set, the kernel process is guaranteed to survive if this process
478 If set, the kernel process is guaranteed to survive if this process
479 dies. If not set, an effort is made to ensure that the kernel is killed
479 dies. If not set, an effort is made to ensure that the kernel is killed
480 when this process dies. Note that in this case it is still good practice
480 when this process dies. Note that in this case it is still good practice
481 to kill kernels manually before exiting.
481 to kill kernels manually before exiting.
482
482
483 Returns
483 Returns
484 -------
484 -------
485 A tuple of form:
485 A tuple of form:
486 (kernel_process, xrep_port, pub_port, req_port)
486 (kernel_process, xrep_port, pub_port, req_port)
487 where kernel_process is a Popen object and the ports are integers.
487 where kernel_process is a Popen object and the ports are integers.
488 """
488 """
489 import socket
489 import socket
490 from subprocess import Popen
490 from subprocess import Popen
491
491
492 # Find open ports as necessary.
492 # Find open ports as necessary.
493 ports = []
493 ports = []
494 ports_needed = int(xrep_port <= 0) + int(pub_port <= 0) + int(req_port <= 0)
494 ports_needed = int(xrep_port <= 0) + int(pub_port <= 0) + int(req_port <= 0)
495 for i in xrange(ports_needed):
495 for i in xrange(ports_needed):
496 sock = socket.socket()
496 sock = socket.socket()
497 sock.bind(('', 0))
497 sock.bind(('', 0))
498 ports.append(sock)
498 ports.append(sock)
499 for i, sock in enumerate(ports):
499 for i, sock in enumerate(ports):
500 port = sock.getsockname()[1]
500 port = sock.getsockname()[1]
501 sock.close()
501 sock.close()
502 ports[i] = port
502 ports[i] = port
503 if xrep_port <= 0:
503 if xrep_port <= 0:
504 xrep_port = ports.pop(0)
504 xrep_port = ports.pop(0)
505 if pub_port <= 0:
505 if pub_port <= 0:
506 pub_port = ports.pop(0)
506 pub_port = ports.pop(0)
507 if req_port <= 0:
507 if req_port <= 0:
508 req_port = ports.pop(0)
508 req_port = ports.pop(0)
509
509
510 # Spawn a kernel.
510 # Spawn a kernel.
511 command = 'from IPython.zmq.kernel import main; main()'
511 command = 'from IPython.zmq.kernel import main; main()'
512 arguments = [ sys.executable, '-c', command, '--xrep', str(xrep_port),
512 arguments = [ sys.executable, '-c', command, '--xrep', str(xrep_port),
513 '--pub', str(pub_port), '--req', str(req_port) ]
513 '--pub', str(pub_port), '--req', str(req_port) ]
514 if independent:
514 if independent:
515 if sys.platform == 'win32':
515 if sys.platform == 'win32':
516 proc = Popen(['start', '/b'] + arguments, shell=True)
516 proc = Popen(['start', '/b'] + arguments, shell=True)
517 else:
517 else:
518 proc = Popen(arguments, preexec_fn=lambda: os.setsid())
518 proc = Popen(arguments, preexec_fn=lambda: os.setsid())
519 else:
519 else:
520 if sys.platform == 'win32':
520 if sys.platform == 'win32':
521 from _subprocess import DuplicateHandle, GetCurrentProcess, \
521 from _subprocess import DuplicateHandle, GetCurrentProcess, \
522 DUPLICATE_SAME_ACCESS
522 DUPLICATE_SAME_ACCESS
523 pid = GetCurrentProcess()
523 pid = GetCurrentProcess()
524 handle = DuplicateHandle(pid, pid, pid, 0,
524 handle = DuplicateHandle(pid, pid, pid, 0,
525 True, # Inheritable by new processes.
525 True, # Inheritable by new processes.
526 DUPLICATE_SAME_ACCESS)
526 DUPLICATE_SAME_ACCESS)
527 proc = Popen(arguments + ['--parent', str(int(handle))])
527 proc = Popen(arguments + ['--parent', str(int(handle))])
528 else:
528 else:
529 proc = Popen(arguments + ['--parent'])
529 proc = Popen(arguments + ['--parent'])
530
530
531 return proc, xrep_port, pub_port, req_port
531 return proc, xrep_port, pub_port, req_port
532
532
533
533
534 if __name__ == '__main__':
534 if __name__ == '__main__':
535 main()
535 main()
General Comments 0
You need to be logged in to leave comments. Login now