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