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