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