##// END OF EJS Templates
* Tab completion now uses the correct cursor position....
epatters -
Show More
@@ -1,1255 +1,1277 b''
1 # Standard library imports
1 # Standard library imports
2 import sys
2 import sys
3 from textwrap import dedent
3 from textwrap import dedent
4
4
5 # System library imports
5 # System library imports
6 from PyQt4 import QtCore, QtGui
6 from PyQt4 import QtCore, QtGui
7
7
8 # Local imports
8 # Local imports
9 from ansi_code_processor import QtAnsiCodeProcessor
9 from ansi_code_processor import QtAnsiCodeProcessor
10 from completion_widget import CompletionWidget
10 from completion_widget import CompletionWidget
11
11
12
12
13 class ConsoleWidget(QtGui.QWidget):
13 class ConsoleWidget(QtGui.QWidget):
14 """ An abstract base class for console-type widgets. This class has
14 """ An abstract base class for console-type widgets. This class has
15 functionality for:
15 functionality for:
16
16
17 * Maintaining a prompt and editing region
17 * Maintaining a prompt and editing region
18 * Providing the traditional Unix-style console keyboard shortcuts
18 * Providing the traditional Unix-style console keyboard shortcuts
19 * Performing tab completion
19 * Performing tab completion
20 * Paging text
20 * Paging text
21 * Handling ANSI escape codes
21 * Handling ANSI escape codes
22
22
23 ConsoleWidget also provides a number of utility methods that will be
23 ConsoleWidget also provides a number of utility methods that will be
24 convenient to implementors of a console-style widget.
24 convenient to implementors of a console-style widget.
25 """
25 """
26
26
27 # Whether to process ANSI escape codes.
27 # Whether to process ANSI escape codes.
28 ansi_codes = True
28 ansi_codes = True
29
29
30 # The maximum number of lines of text before truncation.
30 # The maximum number of lines of text before truncation.
31 buffer_size = 500
31 buffer_size = 500
32
32
33 # Whether to use a list widget or plain text output for tab completion.
33 # Whether to use a list widget or plain text output for tab completion.
34 gui_completion = True
34 gui_completion = True
35
35
36 # Whether to override ShortcutEvents for the keybindings defined by this
36 # Whether to override ShortcutEvents for the keybindings defined by this
37 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
37 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
38 # priority (when it has focus) over, e.g., window-level menu shortcuts.
38 # priority (when it has focus) over, e.g., window-level menu shortcuts.
39 override_shortcuts = False
39 override_shortcuts = False
40
40
41 # Signals that indicate ConsoleWidget state.
41 # Signals that indicate ConsoleWidget state.
42 copy_available = QtCore.pyqtSignal(bool)
42 copy_available = QtCore.pyqtSignal(bool)
43 redo_available = QtCore.pyqtSignal(bool)
43 redo_available = QtCore.pyqtSignal(bool)
44 undo_available = QtCore.pyqtSignal(bool)
44 undo_available = QtCore.pyqtSignal(bool)
45
45
46 # Signal emitted when paging is needed and the paging style has been
46 # Signal emitted when paging is needed and the paging style has been
47 # specified as 'custom'.
47 # specified as 'custom'.
48 custom_page_requested = QtCore.pyqtSignal(object)
48 custom_page_requested = QtCore.pyqtSignal(object)
49
49
50 # Protected class variables.
50 # Protected class variables.
51 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
51 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
52 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
52 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
53 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
53 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
54 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
54 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
55 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
55 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
56 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
56 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
57 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
57 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
58 _shortcuts = set(_ctrl_down_remap.keys() +
58 _shortcuts = set(_ctrl_down_remap.keys() +
59 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
59 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
60
60
61 #---------------------------------------------------------------------------
61 #---------------------------------------------------------------------------
62 # 'QObject' interface
62 # 'QObject' interface
63 #---------------------------------------------------------------------------
63 #---------------------------------------------------------------------------
64
64
65 def __init__(self, kind='plain', paging='inside', parent=None):
65 def __init__(self, kind='plain', paging='inside', parent=None):
66 """ Create a ConsoleWidget.
66 """ Create a ConsoleWidget.
67
67
68 Parameters
68 Parameters
69 ----------
69 ----------
70 kind : str, optional [default 'plain']
70 kind : str, optional [default 'plain']
71 The type of underlying text widget to use. Valid values are 'plain',
71 The type of underlying text widget to use. Valid values are 'plain',
72 which specifies a QPlainTextEdit, and 'rich', which specifies a
72 which specifies a QPlainTextEdit, and 'rich', which specifies a
73 QTextEdit.
73 QTextEdit.
74
74
75 paging : str, optional [default 'inside']
75 paging : str, optional [default 'inside']
76 The type of paging to use. Valid values are:
76 The type of paging to use. Valid values are:
77 'inside' : The widget pages like a traditional terminal pager.
77 'inside' : The widget pages like a traditional terminal pager.
78 'hsplit' : When paging is requested, the widget is split
78 'hsplit' : When paging is requested, the widget is split
79 horizontally. The top pane contains the console,
79 horizontally. The top pane contains the console,
80 and the bottom pane contains the paged text.
80 and the bottom pane contains the paged text.
81 'vsplit' : Similar to 'hsplit', except that a vertical splitter
81 'vsplit' : Similar to 'hsplit', except that a vertical splitter
82 used.
82 used.
83 'custom' : No action is taken by the widget beyond emitting a
83 'custom' : No action is taken by the widget beyond emitting a
84 'custom_page_requested(str)' signal.
84 'custom_page_requested(str)' signal.
85 'none' : The text is written directly to the console.
85 'none' : The text is written directly to the console.
86
86
87 parent : QWidget, optional [default None]
87 parent : QWidget, optional [default None]
88 The parent for this widget.
88 The parent for this widget.
89 """
89 """
90 super(ConsoleWidget, self).__init__(parent)
90 super(ConsoleWidget, self).__init__(parent)
91
91
92 # Create the layout and underlying text widget.
92 # Create the layout and underlying text widget.
93 layout = QtGui.QStackedLayout(self)
93 layout = QtGui.QStackedLayout(self)
94 layout.setMargin(0)
94 layout.setMargin(0)
95 self._control = self._create_control(kind)
95 self._control = self._create_control(kind)
96 self._page_control = None
96 self._page_control = None
97 self._splitter = None
97 self._splitter = None
98 if paging in ('hsplit', 'vsplit'):
98 if paging in ('hsplit', 'vsplit'):
99 self._splitter = QtGui.QSplitter()
99 self._splitter = QtGui.QSplitter()
100 if paging == 'hsplit':
100 if paging == 'hsplit':
101 self._splitter.setOrientation(QtCore.Qt.Horizontal)
101 self._splitter.setOrientation(QtCore.Qt.Horizontal)
102 else:
102 else:
103 self._splitter.setOrientation(QtCore.Qt.Vertical)
103 self._splitter.setOrientation(QtCore.Qt.Vertical)
104 self._splitter.addWidget(self._control)
104 self._splitter.addWidget(self._control)
105 layout.addWidget(self._splitter)
105 layout.addWidget(self._splitter)
106 else:
106 else:
107 layout.addWidget(self._control)
107 layout.addWidget(self._control)
108
108
109 # Create the paging widget, if necessary.
109 # Create the paging widget, if necessary.
110 self._page_style = paging
110 self._page_style = paging
111 if paging in ('inside', 'hsplit', 'vsplit'):
111 if paging in ('inside', 'hsplit', 'vsplit'):
112 self._page_control = self._create_page_control()
112 self._page_control = self._create_page_control()
113 if self._splitter:
113 if self._splitter:
114 self._page_control.hide()
114 self._page_control.hide()
115 self._splitter.addWidget(self._page_control)
115 self._splitter.addWidget(self._page_control)
116 else:
116 else:
117 layout.addWidget(self._page_control)
117 layout.addWidget(self._page_control)
118 elif paging not in ('custom', 'none'):
118 elif paging not in ('custom', 'none'):
119 raise ValueError('Paging style %s unknown.' % repr(paging))
119 raise ValueError('Paging style %s unknown.' % repr(paging))
120
120
121 # Initialize protected variables. Some variables contain useful state
121 # Initialize protected variables. Some variables contain useful state
122 # information for subclasses; they should be considered read-only.
122 # information for subclasses; they should be considered read-only.
123 self._ansi_processor = QtAnsiCodeProcessor()
123 self._ansi_processor = QtAnsiCodeProcessor()
124 self._completion_widget = CompletionWidget(self._control)
124 self._completion_widget = CompletionWidget(self._control)
125 self._continuation_prompt = '> '
125 self._continuation_prompt = '> '
126 self._continuation_prompt_html = None
126 self._continuation_prompt_html = None
127 self._executing = False
127 self._executing = False
128 self._prompt = ''
128 self._prompt = ''
129 self._prompt_html = None
129 self._prompt_html = None
130 self._prompt_pos = 0
130 self._prompt_pos = 0
131 self._reading = False
131 self._reading = False
132 self._reading_callback = None
132 self._reading_callback = None
133 self._tab_width = 8
133 self._tab_width = 8
134
134
135 # Set a monospaced font.
135 # Set a monospaced font.
136 self.reset_font()
136 self.reset_font()
137
137
138 def eventFilter(self, obj, event):
138 def eventFilter(self, obj, event):
139 """ Reimplemented to ensure a console-like behavior in the underlying
139 """ Reimplemented to ensure a console-like behavior in the underlying
140 text widget.
140 text widget.
141 """
141 """
142 # Re-map keys for all filtered widgets.
142 # Re-map keys for all filtered widgets.
143 etype = event.type()
143 etype = event.type()
144 if etype == QtCore.QEvent.KeyPress and \
144 if etype == QtCore.QEvent.KeyPress and \
145 self._control_key_down(event.modifiers()) and \
145 self._control_key_down(event.modifiers()) and \
146 event.key() in self._ctrl_down_remap:
146 event.key() in self._ctrl_down_remap:
147 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
147 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
148 self._ctrl_down_remap[event.key()],
148 self._ctrl_down_remap[event.key()],
149 QtCore.Qt.NoModifier)
149 QtCore.Qt.NoModifier)
150 QtGui.qApp.sendEvent(obj, new_event)
150 QtGui.qApp.sendEvent(obj, new_event)
151 return True
151 return True
152
152
153 # Override shortucts for all filtered widgets. Note that on Mac OS it is
153 # Override shortucts for all filtered widgets. Note that on Mac OS it is
154 # always unnecessary to override shortcuts, hence the check below (users
154 # always unnecessary to override shortcuts, hence the check below (users
155 # should just use the Control key instead of the Command key).
155 # should just use the Control key instead of the Command key).
156 elif etype == QtCore.QEvent.ShortcutOverride and \
156 elif etype == QtCore.QEvent.ShortcutOverride and \
157 sys.platform != 'darwin' and \
157 sys.platform != 'darwin' and \
158 self._control_key_down(event.modifiers()) and \
158 self._control_key_down(event.modifiers()) and \
159 event.key() in self._shortcuts:
159 event.key() in self._shortcuts:
160 event.accept()
160 event.accept()
161 return False
161 return False
162
162
163 elif obj == self._control:
163 elif obj == self._control:
164 # Disable moving text by drag and drop.
164 # Disable moving text by drag and drop.
165 if etype == QtCore.QEvent.DragMove:
165 if etype == QtCore.QEvent.DragMove:
166 return True
166 return True
167
167
168 elif etype == QtCore.QEvent.KeyPress:
168 elif etype == QtCore.QEvent.KeyPress:
169 return self._event_filter_console_keypress(event)
169 return self._event_filter_console_keypress(event)
170
170
171 elif obj == self._page_control:
171 elif obj == self._page_control:
172 if etype == QtCore.QEvent.KeyPress:
172 if etype == QtCore.QEvent.KeyPress:
173 return self._event_filter_page_keypress(event)
173 return self._event_filter_page_keypress(event)
174
174
175 return super(ConsoleWidget, self).eventFilter(obj, event)
175 return super(ConsoleWidget, self).eventFilter(obj, event)
176
176
177 #---------------------------------------------------------------------------
177 #---------------------------------------------------------------------------
178 # 'QWidget' interface
178 # 'QWidget' interface
179 #---------------------------------------------------------------------------
179 #---------------------------------------------------------------------------
180
180
181 def sizeHint(self):
181 def sizeHint(self):
182 """ Reimplemented to suggest a size that is 80 characters wide and
182 """ Reimplemented to suggest a size that is 80 characters wide and
183 25 lines high.
183 25 lines high.
184 """
184 """
185 style = self.style()
185 style = self.style()
186 opt = QtGui.QStyleOptionHeader()
186 opt = QtGui.QStyleOptionHeader()
187 font_metrics = QtGui.QFontMetrics(self.font)
187 font_metrics = QtGui.QFontMetrics(self.font)
188 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
188 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
189
189
190 width = font_metrics.width(' ') * 80
190 width = font_metrics.width(' ') * 80
191 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
191 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
192 if self._page_style == 'hsplit':
192 if self._page_style == 'hsplit':
193 width = width * 2 + splitwidth
193 width = width * 2 + splitwidth
194
194
195 height = font_metrics.height() * 25
195 height = font_metrics.height() * 25
196 if self._page_style == 'vsplit':
196 if self._page_style == 'vsplit':
197 height = height * 2 + splitwidth
197 height = height * 2 + splitwidth
198
198
199 return QtCore.QSize(width, height)
199 return QtCore.QSize(width, height)
200
200
201 #---------------------------------------------------------------------------
201 #---------------------------------------------------------------------------
202 # 'ConsoleWidget' public interface
202 # 'ConsoleWidget' public interface
203 #---------------------------------------------------------------------------
203 #---------------------------------------------------------------------------
204
204
205 def can_paste(self):
205 def can_paste(self):
206 """ Returns whether text can be pasted from the clipboard.
206 """ Returns whether text can be pasted from the clipboard.
207 """
207 """
208 # Accept only text that can be ASCII encoded.
208 # Accept only text that can be ASCII encoded.
209 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
209 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
210 text = QtGui.QApplication.clipboard().text()
210 text = QtGui.QApplication.clipboard().text()
211 if not text.isEmpty():
211 if not text.isEmpty():
212 try:
212 try:
213 str(text)
213 str(text)
214 return True
214 return True
215 except UnicodeEncodeError:
215 except UnicodeEncodeError:
216 pass
216 pass
217 return False
217 return False
218
218
219 def clear(self, keep_input=False):
219 def clear(self, keep_input=False):
220 """ Clear the console, then write a new prompt. If 'keep_input' is set,
220 """ Clear the console, then write a new prompt. If 'keep_input' is set,
221 restores the old input buffer when the new prompt is written.
221 restores the old input buffer when the new prompt is written.
222 """
222 """
223 self._control.clear()
223 self._control.clear()
224 if keep_input:
224 if keep_input:
225 input_buffer = self.input_buffer
225 input_buffer = self.input_buffer
226 self._show_prompt()
226 self._show_prompt()
227 if keep_input:
227 if keep_input:
228 self.input_buffer = input_buffer
228 self.input_buffer = input_buffer
229
229
230 def copy(self):
230 def copy(self):
231 """ Copy the current selected text to the clipboard.
231 """ Copy the current selected text to the clipboard.
232 """
232 """
233 self._control.copy()
233 self._control.copy()
234
234
235 def execute(self, source=None, hidden=False, interactive=False):
235 def execute(self, source=None, hidden=False, interactive=False):
236 """ Executes source or the input buffer, possibly prompting for more
236 """ Executes source or the input buffer, possibly prompting for more
237 input.
237 input.
238
238
239 Parameters:
239 Parameters:
240 -----------
240 -----------
241 source : str, optional
241 source : str, optional
242
242
243 The source to execute. If not specified, the input buffer will be
243 The source to execute. If not specified, the input buffer will be
244 used. If specified and 'hidden' is False, the input buffer will be
244 used. If specified and 'hidden' is False, the input buffer will be
245 replaced with the source before execution.
245 replaced with the source before execution.
246
246
247 hidden : bool, optional (default False)
247 hidden : bool, optional (default False)
248
248
249 If set, no output will be shown and the prompt will not be modified.
249 If set, no output will be shown and the prompt will not be modified.
250 In other words, it will be completely invisible to the user that
250 In other words, it will be completely invisible to the user that
251 an execution has occurred.
251 an execution has occurred.
252
252
253 interactive : bool, optional (default False)
253 interactive : bool, optional (default False)
254
254
255 Whether the console is to treat the source as having been manually
255 Whether the console is to treat the source as having been manually
256 entered by the user. The effect of this parameter depends on the
256 entered by the user. The effect of this parameter depends on the
257 subclass implementation.
257 subclass implementation.
258
258
259 Raises:
259 Raises:
260 -------
260 -------
261 RuntimeError
261 RuntimeError
262 If incomplete input is given and 'hidden' is True. In this case,
262 If incomplete input is given and 'hidden' is True. In this case,
263 it is not possible to prompt for more input.
263 it is not possible to prompt for more input.
264
264
265 Returns:
265 Returns:
266 --------
266 --------
267 A boolean indicating whether the source was executed.
267 A boolean indicating whether the source was executed.
268 """
268 """
269 if not hidden:
269 if not hidden:
270 if source is not None:
270 if source is not None:
271 self.input_buffer = source
271 self.input_buffer = source
272
272
273 self._append_plain_text('\n')
273 self._append_plain_text('\n')
274 self._executing_input_buffer = self.input_buffer
274 self._executing_input_buffer = self.input_buffer
275 self._executing = True
275 self._executing = True
276 self._prompt_finished()
276 self._prompt_finished()
277
277
278 real_source = self.input_buffer if source is None else source
278 real_source = self.input_buffer if source is None else source
279 complete = self._is_complete(real_source, interactive)
279 complete = self._is_complete(real_source, interactive)
280 if complete:
280 if complete:
281 if not hidden:
281 if not hidden:
282 # The maximum block count is only in effect during execution.
282 # The maximum block count is only in effect during execution.
283 # This ensures that _prompt_pos does not become invalid due to
283 # This ensures that _prompt_pos does not become invalid due to
284 # text truncation.
284 # text truncation.
285 self._control.document().setMaximumBlockCount(self.buffer_size)
285 self._control.document().setMaximumBlockCount(self.buffer_size)
286 self._execute(real_source, hidden)
286 self._execute(real_source, hidden)
287 elif hidden:
287 elif hidden:
288 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
288 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
289 else:
289 else:
290 self._show_continuation_prompt()
290 self._show_continuation_prompt()
291
291
292 return complete
292 return complete
293
293
294 def _get_input_buffer(self):
294 def _get_input_buffer(self):
295 """ The text that the user has entered entered at the current prompt.
295 """ The text that the user has entered entered at the current prompt.
296 """
296 """
297 # If we're executing, the input buffer may not even exist anymore due to
297 # If we're executing, the input buffer may not even exist anymore due to
298 # the limit imposed by 'buffer_size'. Therefore, we store it.
298 # the limit imposed by 'buffer_size'. Therefore, we store it.
299 if self._executing:
299 if self._executing:
300 return self._executing_input_buffer
300 return self._executing_input_buffer
301
301
302 cursor = self._get_end_cursor()
302 cursor = self._get_end_cursor()
303 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
303 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
304 input_buffer = str(cursor.selection().toPlainText())
304 input_buffer = str(cursor.selection().toPlainText())
305
305
306 # Strip out continuation prompts.
306 # Strip out continuation prompts.
307 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
307 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
308
308
309 def _set_input_buffer(self, string):
309 def _set_input_buffer(self, string):
310 """ Replaces the text in the input buffer with 'string'.
310 """ Replaces the text in the input buffer with 'string'.
311 """
311 """
312 # For now, it is an error to modify the input buffer during execution.
312 # For now, it is an error to modify the input buffer during execution.
313 if self._executing:
313 if self._executing:
314 raise RuntimeError("Cannot change input buffer during execution.")
314 raise RuntimeError("Cannot change input buffer during execution.")
315
315
316 # Remove old text.
316 # Remove old text.
317 cursor = self._get_end_cursor()
317 cursor = self._get_end_cursor()
318 cursor.beginEditBlock()
318 cursor.beginEditBlock()
319 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
319 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
320 cursor.removeSelectedText()
320 cursor.removeSelectedText()
321
321
322 # Insert new text with continuation prompts.
322 # Insert new text with continuation prompts.
323 lines = string.splitlines(True)
323 lines = string.splitlines(True)
324 if lines:
324 if lines:
325 self._append_plain_text(lines[0])
325 self._append_plain_text(lines[0])
326 for i in xrange(1, len(lines)):
326 for i in xrange(1, len(lines)):
327 if self._continuation_prompt_html is None:
327 if self._continuation_prompt_html is None:
328 self._append_plain_text(self._continuation_prompt)
328 self._append_plain_text(self._continuation_prompt)
329 else:
329 else:
330 self._append_html(self._continuation_prompt_html)
330 self._append_html(self._continuation_prompt_html)
331 self._append_plain_text(lines[i])
331 self._append_plain_text(lines[i])
332 cursor.endEditBlock()
332 cursor.endEditBlock()
333 self._control.moveCursor(QtGui.QTextCursor.End)
333 self._control.moveCursor(QtGui.QTextCursor.End)
334
334
335 input_buffer = property(_get_input_buffer, _set_input_buffer)
335 input_buffer = property(_get_input_buffer, _set_input_buffer)
336
336
337 def _get_font(self):
337 def _get_font(self):
338 """ The base font being used by the ConsoleWidget.
338 """ The base font being used by the ConsoleWidget.
339 """
339 """
340 return self._control.document().defaultFont()
340 return self._control.document().defaultFont()
341
341
342 def _set_font(self, font):
342 def _set_font(self, font):
343 """ Sets the base font for the ConsoleWidget to the specified QFont.
343 """ Sets the base font for the ConsoleWidget to the specified QFont.
344 """
344 """
345 font_metrics = QtGui.QFontMetrics(font)
345 font_metrics = QtGui.QFontMetrics(font)
346 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
346 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
347
347
348 self._completion_widget.setFont(font)
348 self._completion_widget.setFont(font)
349 self._control.document().setDefaultFont(font)
349 self._control.document().setDefaultFont(font)
350 if self._page_control:
350 if self._page_control:
351 self._page_control.document().setDefaultFont(font)
351 self._page_control.document().setDefaultFont(font)
352
352
353 font = property(_get_font, _set_font)
353 font = property(_get_font, _set_font)
354
354
355 def paste(self):
355 def paste(self):
356 """ Paste the contents of the clipboard into the input region.
356 """ Paste the contents of the clipboard into the input region.
357 """
357 """
358 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
358 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
359 try:
359 try:
360 text = str(QtGui.QApplication.clipboard().text())
360 text = str(QtGui.QApplication.clipboard().text())
361 except UnicodeEncodeError:
361 except UnicodeEncodeError:
362 pass
362 pass
363 else:
363 else:
364 self._insert_plain_text_into_buffer(dedent(text))
364 self._insert_plain_text_into_buffer(dedent(text))
365
365
366 def print_(self, printer):
366 def print_(self, printer):
367 """ Print the contents of the ConsoleWidget to the specified QPrinter.
367 """ Print the contents of the ConsoleWidget to the specified QPrinter.
368 """
368 """
369 self._control.print_(printer)
369 self._control.print_(printer)
370
370
371 def redo(self):
371 def redo(self):
372 """ Redo the last operation. If there is no operation to redo, nothing
372 """ Redo the last operation. If there is no operation to redo, nothing
373 happens.
373 happens.
374 """
374 """
375 self._control.redo()
375 self._control.redo()
376
376
377 def reset_font(self):
377 def reset_font(self):
378 """ Sets the font to the default fixed-width font for this platform.
378 """ Sets the font to the default fixed-width font for this platform.
379 """
379 """
380 if sys.platform == 'win32':
380 if sys.platform == 'win32':
381 name = 'Courier'
381 name = 'Courier'
382 elif sys.platform == 'darwin':
382 elif sys.platform == 'darwin':
383 name = 'Monaco'
383 name = 'Monaco'
384 else:
384 else:
385 name = 'Monospace'
385 name = 'Monospace'
386 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
386 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
387 font.setStyleHint(QtGui.QFont.TypeWriter)
387 font.setStyleHint(QtGui.QFont.TypeWriter)
388 self._set_font(font)
388 self._set_font(font)
389
389
390 def select_all(self):
390 def select_all(self):
391 """ Selects all the text in the buffer.
391 """ Selects all the text in the buffer.
392 """
392 """
393 self._control.selectAll()
393 self._control.selectAll()
394
394
395 def _get_tab_width(self):
395 def _get_tab_width(self):
396 """ The width (in terms of space characters) for tab characters.
396 """ The width (in terms of space characters) for tab characters.
397 """
397 """
398 return self._tab_width
398 return self._tab_width
399
399
400 def _set_tab_width(self, tab_width):
400 def _set_tab_width(self, tab_width):
401 """ Sets the width (in terms of space characters) for tab characters.
401 """ Sets the width (in terms of space characters) for tab characters.
402 """
402 """
403 font_metrics = QtGui.QFontMetrics(self.font)
403 font_metrics = QtGui.QFontMetrics(self.font)
404 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
404 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
405
405
406 self._tab_width = tab_width
406 self._tab_width = tab_width
407
407
408 tab_width = property(_get_tab_width, _set_tab_width)
408 tab_width = property(_get_tab_width, _set_tab_width)
409
409
410 def undo(self):
410 def undo(self):
411 """ Undo the last operation. If there is no operation to undo, nothing
411 """ Undo the last operation. If there is no operation to undo, nothing
412 happens.
412 happens.
413 """
413 """
414 self._control.undo()
414 self._control.undo()
415
415
416 #---------------------------------------------------------------------------
416 #---------------------------------------------------------------------------
417 # 'ConsoleWidget' abstract interface
417 # 'ConsoleWidget' abstract interface
418 #---------------------------------------------------------------------------
418 #---------------------------------------------------------------------------
419
419
420 def _is_complete(self, source, interactive):
420 def _is_complete(self, source, interactive):
421 """ Returns whether 'source' can be executed. When triggered by an
421 """ Returns whether 'source' can be executed. When triggered by an
422 Enter/Return key press, 'interactive' is True; otherwise, it is
422 Enter/Return key press, 'interactive' is True; otherwise, it is
423 False.
423 False.
424 """
424 """
425 raise NotImplementedError
425 raise NotImplementedError
426
426
427 def _execute(self, source, hidden):
427 def _execute(self, source, hidden):
428 """ Execute 'source'. If 'hidden', do not show any output.
428 """ Execute 'source'. If 'hidden', do not show any output.
429 """
429 """
430 raise NotImplementedError
430 raise NotImplementedError
431
431
432 def _execute_interrupt(self):
432 def _execute_interrupt(self):
433 """ Attempts to stop execution. Returns whether this method has an
433 """ Attempts to stop execution. Returns whether this method has an
434 implementation.
434 implementation.
435 """
435 """
436 return False
436 return False
437
437
438 def _prompt_started_hook(self):
438 def _prompt_started_hook(self):
439 """ Called immediately after a new prompt is displayed.
439 """ Called immediately after a new prompt is displayed.
440 """
440 """
441 pass
441 pass
442
442
443 def _prompt_finished_hook(self):
443 def _prompt_finished_hook(self):
444 """ Called immediately after a prompt is finished, i.e. when some input
444 """ Called immediately after a prompt is finished, i.e. when some input
445 will be processed and a new prompt displayed.
445 will be processed and a new prompt displayed.
446 """
446 """
447 pass
447 pass
448
448
449 def _up_pressed(self):
449 def _up_pressed(self):
450 """ Called when the up key is pressed. Returns whether to continue
450 """ Called when the up key is pressed. Returns whether to continue
451 processing the event.
451 processing the event.
452 """
452 """
453 return True
453 return True
454
454
455 def _down_pressed(self):
455 def _down_pressed(self):
456 """ Called when the down key is pressed. Returns whether to continue
456 """ Called when the down key is pressed. Returns whether to continue
457 processing the event.
457 processing the event.
458 """
458 """
459 return True
459 return True
460
460
461 def _tab_pressed(self):
461 def _tab_pressed(self):
462 """ Called when the tab key is pressed. Returns whether to continue
462 """ Called when the tab key is pressed. Returns whether to continue
463 processing the event.
463 processing the event.
464 """
464 """
465 return False
465 return False
466
466
467 #--------------------------------------------------------------------------
467 #--------------------------------------------------------------------------
468 # 'ConsoleWidget' protected interface
468 # 'ConsoleWidget' protected interface
469 #--------------------------------------------------------------------------
469 #--------------------------------------------------------------------------
470
470
471 def _append_html(self, html):
471 def _append_html(self, html):
472 """ Appends html at the end of the console buffer.
472 """ Appends html at the end of the console buffer.
473 """
473 """
474 cursor = self._get_end_cursor()
474 cursor = self._get_end_cursor()
475 self._insert_html(cursor, html)
475 self._insert_html(cursor, html)
476
476
477 def _append_html_fetching_plain_text(self, html):
477 def _append_html_fetching_plain_text(self, html):
478 """ Appends 'html', then returns the plain text version of it.
478 """ Appends 'html', then returns the plain text version of it.
479 """
479 """
480 cursor = self._get_end_cursor()
480 cursor = self._get_end_cursor()
481 return self._insert_html_fetching_plain_text(cursor, html)
481 return self._insert_html_fetching_plain_text(cursor, html)
482
482
483 def _append_plain_text(self, text):
483 def _append_plain_text(self, text):
484 """ Appends plain text at the end of the console buffer, processing
484 """ Appends plain text at the end of the console buffer, processing
485 ANSI codes if enabled.
485 ANSI codes if enabled.
486 """
486 """
487 cursor = self._get_end_cursor()
487 cursor = self._get_end_cursor()
488 self._insert_plain_text(cursor, text)
488 self._insert_plain_text(cursor, text)
489
489
490 def _append_plain_text_keeping_prompt(self, text):
490 def _append_plain_text_keeping_prompt(self, text):
491 """ Writes 'text' after the current prompt, then restores the old prompt
491 """ Writes 'text' after the current prompt, then restores the old prompt
492 with its old input buffer.
492 with its old input buffer.
493 """
493 """
494 input_buffer = self.input_buffer
494 input_buffer = self.input_buffer
495 self._append_plain_text('\n')
495 self._append_plain_text('\n')
496 self._prompt_finished()
496 self._prompt_finished()
497
497
498 self._append_plain_text(text)
498 self._append_plain_text(text)
499 self._show_prompt()
499 self._show_prompt()
500 self.input_buffer = input_buffer
500 self.input_buffer = input_buffer
501
501
502 def _complete_with_items(self, cursor, items):
502 def _complete_with_items(self, cursor, items):
503 """ Performs completion with 'items' at the specified cursor location.
503 """ Performs completion with 'items' at the specified cursor location.
504 """
504 """
505 if len(items) == 1:
505 if len(items) == 1:
506 cursor.setPosition(self._control.textCursor().position(),
506 cursor.setPosition(self._control.textCursor().position(),
507 QtGui.QTextCursor.KeepAnchor)
507 QtGui.QTextCursor.KeepAnchor)
508 cursor.insertText(items[0])
508 cursor.insertText(items[0])
509 elif len(items) > 1:
509 elif len(items) > 1:
510 if self.gui_completion:
510 if self.gui_completion:
511 self._completion_widget.show_items(cursor, items)
511 self._completion_widget.show_items(cursor, items)
512 else:
512 else:
513 text = self._format_as_columns(items)
513 text = self._format_as_columns(items)
514 self._append_plain_text_keeping_prompt(text)
514 self._append_plain_text_keeping_prompt(text)
515
515
516 def _control_key_down(self, modifiers):
516 def _control_key_down(self, modifiers):
517 """ Given a KeyboardModifiers flags object, return whether the Control
517 """ Given a KeyboardModifiers flags object, return whether the Control
518 key is down (on Mac OS, treat the Command key as a synonym for
518 key is down (on Mac OS, treat the Command key as a synonym for
519 Control).
519 Control).
520 """
520 """
521 down = bool(modifiers & QtCore.Qt.ControlModifier)
521 down = bool(modifiers & QtCore.Qt.ControlModifier)
522
522
523 # Note: on Mac OS, ControlModifier corresponds to the Command key while
523 # Note: on Mac OS, ControlModifier corresponds to the Command key while
524 # MetaModifier corresponds to the Control key.
524 # MetaModifier corresponds to the Control key.
525 if sys.platform == 'darwin':
525 if sys.platform == 'darwin':
526 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
526 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
527
527
528 return down
528 return down
529
529
530 def _create_control(self, kind):
530 def _create_control(self, kind):
531 """ Creates and connects the underlying text widget.
531 """ Creates and connects the underlying text widget.
532 """
532 """
533 if kind == 'plain':
533 if kind == 'plain':
534 control = QtGui.QPlainTextEdit()
534 control = QtGui.QPlainTextEdit()
535 elif kind == 'rich':
535 elif kind == 'rich':
536 control = QtGui.QTextEdit()
536 control = QtGui.QTextEdit()
537 control.setAcceptRichText(False)
537 control.setAcceptRichText(False)
538 else:
538 else:
539 raise ValueError("Kind %s unknown." % repr(kind))
539 raise ValueError("Kind %s unknown." % repr(kind))
540 control.installEventFilter(self)
540 control.installEventFilter(self)
541 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
541 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
542 control.customContextMenuRequested.connect(self._show_context_menu)
542 control.customContextMenuRequested.connect(self._show_context_menu)
543 control.copyAvailable.connect(self.copy_available)
543 control.copyAvailable.connect(self.copy_available)
544 control.redoAvailable.connect(self.redo_available)
544 control.redoAvailable.connect(self.redo_available)
545 control.undoAvailable.connect(self.undo_available)
545 control.undoAvailable.connect(self.undo_available)
546 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
546 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
547 return control
547 return control
548
548
549 def _create_page_control(self):
549 def _create_page_control(self):
550 """ Creates and connects the underlying paging widget.
550 """ Creates and connects the underlying paging widget.
551 """
551 """
552 control = QtGui.QPlainTextEdit()
552 control = QtGui.QPlainTextEdit()
553 control.installEventFilter(self)
553 control.installEventFilter(self)
554 control.setReadOnly(True)
554 control.setReadOnly(True)
555 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
555 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
556 return control
556 return control
557
557
558 def _event_filter_console_keypress(self, event):
558 def _event_filter_console_keypress(self, event):
559 """ Filter key events for the underlying text widget to create a
559 """ Filter key events for the underlying text widget to create a
560 console-like interface.
560 console-like interface.
561 """
561 """
562 intercepted = False
562 intercepted = False
563 cursor = self._control.textCursor()
563 cursor = self._control.textCursor()
564 position = cursor.position()
564 position = cursor.position()
565 key = event.key()
565 key = event.key()
566 ctrl_down = self._control_key_down(event.modifiers())
566 ctrl_down = self._control_key_down(event.modifiers())
567 alt_down = event.modifiers() & QtCore.Qt.AltModifier
567 alt_down = event.modifiers() & QtCore.Qt.AltModifier
568 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
568 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
569
569
570 if event.matches(QtGui.QKeySequence.Paste):
570 if event.matches(QtGui.QKeySequence.Paste):
571 # Call our paste instead of the underlying text widget's.
571 # Call our paste instead of the underlying text widget's.
572 self.paste()
572 self.paste()
573 intercepted = True
573 intercepted = True
574
574
575 elif ctrl_down:
575 elif ctrl_down:
576 if key == QtCore.Qt.Key_C:
576 if key == QtCore.Qt.Key_C:
577 intercepted = self._executing and self._execute_interrupt()
577 intercepted = self._executing and self._execute_interrupt()
578
578
579 elif key == QtCore.Qt.Key_K:
579 elif key == QtCore.Qt.Key_K:
580 if self._in_buffer(position):
580 if self._in_buffer(position):
581 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
581 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
582 QtGui.QTextCursor.KeepAnchor)
582 QtGui.QTextCursor.KeepAnchor)
583 cursor.removeSelectedText()
583 cursor.removeSelectedText()
584 intercepted = True
584 intercepted = True
585
585
586 elif key == QtCore.Qt.Key_X:
586 elif key == QtCore.Qt.Key_X:
587 intercepted = True
587 intercepted = True
588
588
589 elif key == QtCore.Qt.Key_Y:
589 elif key == QtCore.Qt.Key_Y:
590 self.paste()
590 self.paste()
591 intercepted = True
591 intercepted = True
592
592
593 elif alt_down:
593 elif alt_down:
594 if key == QtCore.Qt.Key_B:
594 if key == QtCore.Qt.Key_B:
595 self._set_cursor(self._get_word_start_cursor(position))
595 self._set_cursor(self._get_word_start_cursor(position))
596 intercepted = True
596 intercepted = True
597
597
598 elif key == QtCore.Qt.Key_F:
598 elif key == QtCore.Qt.Key_F:
599 self._set_cursor(self._get_word_end_cursor(position))
599 self._set_cursor(self._get_word_end_cursor(position))
600 intercepted = True
600 intercepted = True
601
601
602 elif key == QtCore.Qt.Key_Backspace:
602 elif key == QtCore.Qt.Key_Backspace:
603 cursor = self._get_word_start_cursor(position)
603 cursor = self._get_word_start_cursor(position)
604 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
604 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
605 cursor.removeSelectedText()
605 cursor.removeSelectedText()
606 intercepted = True
606 intercepted = True
607
607
608 elif key == QtCore.Qt.Key_D:
608 elif key == QtCore.Qt.Key_D:
609 cursor = self._get_word_end_cursor(position)
609 cursor = self._get_word_end_cursor(position)
610 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
610 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
611 cursor.removeSelectedText()
611 cursor.removeSelectedText()
612 intercepted = True
612 intercepted = True
613
613
614 else:
614 else:
615 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
615 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
616 if self._reading:
616 if self._reading:
617 self._append_plain_text('\n')
617 self._append_plain_text('\n')
618 self._reading = False
618 self._reading = False
619 if self._reading_callback:
619 if self._reading_callback:
620 self._reading_callback()
620 self._reading_callback()
621 elif not self._executing:
621 elif not self._executing:
622 self.execute(interactive=True)
622 self.execute(interactive=True)
623 intercepted = True
623 intercepted = True
624
624
625 elif key == QtCore.Qt.Key_Up:
625 elif key == QtCore.Qt.Key_Up:
626 if self._reading or not self._up_pressed():
626 if self._reading or not self._up_pressed():
627 intercepted = True
627 intercepted = True
628 else:
628 else:
629 prompt_line = self._get_prompt_cursor().blockNumber()
629 prompt_line = self._get_prompt_cursor().blockNumber()
630 intercepted = cursor.blockNumber() <= prompt_line
630 intercepted = cursor.blockNumber() <= prompt_line
631
631
632 elif key == QtCore.Qt.Key_Down:
632 elif key == QtCore.Qt.Key_Down:
633 if self._reading or not self._down_pressed():
633 if self._reading or not self._down_pressed():
634 intercepted = True
634 intercepted = True
635 else:
635 else:
636 end_line = self._get_end_cursor().blockNumber()
636 end_line = self._get_end_cursor().blockNumber()
637 intercepted = cursor.blockNumber() == end_line
637 intercepted = cursor.blockNumber() == end_line
638
638
639 elif key == QtCore.Qt.Key_Tab:
639 elif key == QtCore.Qt.Key_Tab:
640 if self._reading:
640 if self._reading:
641 intercepted = False
641 intercepted = False
642 else:
642 else:
643 intercepted = not self._tab_pressed()
643 intercepted = not self._tab_pressed()
644
644
645 elif key == QtCore.Qt.Key_Left:
645 elif key == QtCore.Qt.Key_Left:
646 intercepted = not self._in_buffer(position - 1)
646 intercepted = not self._in_buffer(position - 1)
647
647
648 elif key == QtCore.Qt.Key_Home:
648 elif key == QtCore.Qt.Key_Home:
649 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
649 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
650 start_line = cursor.blockNumber()
650 start_line = cursor.blockNumber()
651 if start_line == self._get_prompt_cursor().blockNumber():
651 if start_line == self._get_prompt_cursor().blockNumber():
652 start_pos = self._prompt_pos
652 start_pos = self._prompt_pos
653 else:
653 else:
654 start_pos = cursor.position()
654 start_pos = cursor.position()
655 start_pos += len(self._continuation_prompt)
655 start_pos += len(self._continuation_prompt)
656 if shift_down and self._in_buffer(position):
656 if shift_down and self._in_buffer(position):
657 self._set_selection(position, start_pos)
657 self._set_selection(position, start_pos)
658 else:
658 else:
659 self._set_position(start_pos)
659 self._set_position(start_pos)
660 intercepted = True
660 intercepted = True
661
661
662 elif key == QtCore.Qt.Key_Backspace:
662 elif key == QtCore.Qt.Key_Backspace:
663
663
664 # Line deletion (remove continuation prompt)
664 # Line deletion (remove continuation prompt)
665 len_prompt = len(self._continuation_prompt)
665 len_prompt = len(self._continuation_prompt)
666 if not self._reading and \
666 if not self._reading and \
667 cursor.columnNumber() == len_prompt and \
667 cursor.columnNumber() == len_prompt and \
668 position != self._prompt_pos:
668 position != self._prompt_pos:
669 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
669 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
670 QtGui.QTextCursor.KeepAnchor)
670 QtGui.QTextCursor.KeepAnchor)
671 cursor.removeSelectedText()
671 cursor.removeSelectedText()
672 cursor.deletePreviousChar()
672 cursor.deletePreviousChar()
673 intercepted = True
673 intercepted = True
674
674
675 # Regular backwards deletion
675 # Regular backwards deletion
676 else:
676 else:
677 anchor = cursor.anchor()
677 anchor = cursor.anchor()
678 if anchor == position:
678 if anchor == position:
679 intercepted = not self._in_buffer(position - 1)
679 intercepted = not self._in_buffer(position - 1)
680 else:
680 else:
681 intercepted = not self._in_buffer(min(anchor, position))
681 intercepted = not self._in_buffer(min(anchor, position))
682
682
683 elif key == QtCore.Qt.Key_Delete:
683 elif key == QtCore.Qt.Key_Delete:
684 anchor = cursor.anchor()
684 anchor = cursor.anchor()
685 intercepted = not self._in_buffer(min(anchor, position))
685 intercepted = not self._in_buffer(min(anchor, position))
686
686
687 # Don't move the cursor if control is down to allow copy-paste using
687 # Don't move the cursor if control is down to allow copy-paste using
688 # the keyboard in any part of the buffer.
688 # the keyboard in any part of the buffer.
689 if not ctrl_down:
689 if not ctrl_down:
690 self._keep_cursor_in_buffer()
690 self._keep_cursor_in_buffer()
691
691
692 return intercepted
692 return intercepted
693
693
694 def _event_filter_page_keypress(self, event):
694 def _event_filter_page_keypress(self, event):
695 """ Filter key events for the paging widget to create console-like
695 """ Filter key events for the paging widget to create console-like
696 interface.
696 interface.
697 """
697 """
698 key = event.key()
698 key = event.key()
699
699
700 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
700 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
701 if self._splitter:
701 if self._splitter:
702 self._page_control.hide()
702 self._page_control.hide()
703 else:
703 else:
704 self.layout().setCurrentWidget(self._control)
704 self.layout().setCurrentWidget(self._control)
705 return True
705 return True
706
706
707 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
707 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
708 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
708 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
709 QtCore.Qt.Key_Down,
709 QtCore.Qt.Key_Down,
710 QtCore.Qt.NoModifier)
710 QtCore.Qt.NoModifier)
711 QtGui.qApp.sendEvent(self._page_control, new_event)
711 QtGui.qApp.sendEvent(self._page_control, new_event)
712 return True
712 return True
713
713
714 return False
714 return False
715
715
716 def _format_as_columns(self, items, separator=' '):
716 def _format_as_columns(self, items, separator=' '):
717 """ Transform a list of strings into a single string with columns.
717 """ Transform a list of strings into a single string with columns.
718
718
719 Parameters
719 Parameters
720 ----------
720 ----------
721 items : sequence of strings
721 items : sequence of strings
722 The strings to process.
722 The strings to process.
723
723
724 separator : str, optional [default is two spaces]
724 separator : str, optional [default is two spaces]
725 The string that separates columns.
725 The string that separates columns.
726
726
727 Returns
727 Returns
728 -------
728 -------
729 The formatted string.
729 The formatted string.
730 """
730 """
731 # Note: this code is adapted from columnize 0.3.2.
731 # Note: this code is adapted from columnize 0.3.2.
732 # See http://code.google.com/p/pycolumnize/
732 # See http://code.google.com/p/pycolumnize/
733
733
734 width = self._control.viewport().width()
734 width = self._control.viewport().width()
735 char_width = QtGui.QFontMetrics(self.font).width(' ')
735 char_width = QtGui.QFontMetrics(self.font).width(' ')
736 displaywidth = max(5, width / char_width)
736 displaywidth = max(5, width / char_width)
737
737
738 # Some degenerate cases.
738 # Some degenerate cases.
739 size = len(items)
739 size = len(items)
740 if size == 0:
740 if size == 0:
741 return '\n'
741 return '\n'
742 elif size == 1:
742 elif size == 1:
743 return '%s\n' % str(items[0])
743 return '%s\n' % str(items[0])
744
744
745 # Try every row count from 1 upwards
745 # Try every row count from 1 upwards
746 array_index = lambda nrows, row, col: nrows*col + row
746 array_index = lambda nrows, row, col: nrows*col + row
747 for nrows in range(1, size):
747 for nrows in range(1, size):
748 ncols = (size + nrows - 1) // nrows
748 ncols = (size + nrows - 1) // nrows
749 colwidths = []
749 colwidths = []
750 totwidth = -len(separator)
750 totwidth = -len(separator)
751 for col in range(ncols):
751 for col in range(ncols):
752 # Get max column width for this column
752 # Get max column width for this column
753 colwidth = 0
753 colwidth = 0
754 for row in range(nrows):
754 for row in range(nrows):
755 i = array_index(nrows, row, col)
755 i = array_index(nrows, row, col)
756 if i >= size: break
756 if i >= size: break
757 x = items[i]
757 x = items[i]
758 colwidth = max(colwidth, len(x))
758 colwidth = max(colwidth, len(x))
759 colwidths.append(colwidth)
759 colwidths.append(colwidth)
760 totwidth += colwidth + len(separator)
760 totwidth += colwidth + len(separator)
761 if totwidth > displaywidth:
761 if totwidth > displaywidth:
762 break
762 break
763 if totwidth <= displaywidth:
763 if totwidth <= displaywidth:
764 break
764 break
765
765
766 # The smallest number of rows computed and the max widths for each
766 # The smallest number of rows computed and the max widths for each
767 # column has been obtained. Now we just have to format each of the rows.
767 # column has been obtained. Now we just have to format each of the rows.
768 string = ''
768 string = ''
769 for row in range(nrows):
769 for row in range(nrows):
770 texts = []
770 texts = []
771 for col in range(ncols):
771 for col in range(ncols):
772 i = row + nrows*col
772 i = row + nrows*col
773 if i >= size:
773 if i >= size:
774 texts.append('')
774 texts.append('')
775 else:
775 else:
776 texts.append(items[i])
776 texts.append(items[i])
777 while texts and not texts[-1]:
777 while texts and not texts[-1]:
778 del texts[-1]
778 del texts[-1]
779 for col in range(len(texts)):
779 for col in range(len(texts)):
780 texts[col] = texts[col].ljust(colwidths[col])
780 texts[col] = texts[col].ljust(colwidths[col])
781 string += '%s\n' % str(separator.join(texts))
781 string += '%s\n' % str(separator.join(texts))
782 return string
782 return string
783
783
784 def _get_block_plain_text(self, block):
784 def _get_block_plain_text(self, block):
785 """ Given a QTextBlock, return its unformatted text.
785 """ Given a QTextBlock, return its unformatted text.
786 """
786 """
787 cursor = QtGui.QTextCursor(block)
787 cursor = QtGui.QTextCursor(block)
788 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
788 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
789 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
789 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
790 QtGui.QTextCursor.KeepAnchor)
790 QtGui.QTextCursor.KeepAnchor)
791 return str(cursor.selection().toPlainText())
791 return str(cursor.selection().toPlainText())
792
792
793 def _get_cursor(self):
793 def _get_cursor(self):
794 """ Convenience method that returns a cursor for the current position.
794 """ Convenience method that returns a cursor for the current position.
795 """
795 """
796 return self._control.textCursor()
796 return self._control.textCursor()
797
797
798 def _get_end_cursor(self):
798 def _get_end_cursor(self):
799 """ Convenience method that returns a cursor for the last character.
799 """ Convenience method that returns a cursor for the last character.
800 """
800 """
801 cursor = self._control.textCursor()
801 cursor = self._control.textCursor()
802 cursor.movePosition(QtGui.QTextCursor.End)
802 cursor.movePosition(QtGui.QTextCursor.End)
803 return cursor
803 return cursor
804
804
805 def _get_input_buffer_cursor_column(self):
806 """ Returns the column of the cursor in the input buffer, excluding the
807 contribution by the prompt, or -1 if there is no such column.
808 """
809 prompt = self._get_input_buffer_cursor_prompt()
810 if prompt is None:
811 return -1
812 else:
813 cursor = self._control.textCursor()
814 return cursor.columnNumber() - len(prompt)
815
805 def _get_input_buffer_cursor_line(self):
816 def _get_input_buffer_cursor_line(self):
806 """ The text in the line of the input buffer in which the user's cursor
817 """ Returns line of the input buffer that contains the cursor, or None
807 rests. Returns a string if there is such a line; otherwise, None.
818 if there is no such line.
819 """
820 prompt = self._get_input_buffer_cursor_prompt()
821 if prompt is None:
822 return None
823 else:
824 cursor = self._control.textCursor()
825 text = self._get_block_plain_text(cursor.block())
826 return text[len(prompt):]
827
828 def _get_input_buffer_cursor_prompt(self):
829 """ Returns the (plain text) prompt for line of the input buffer that
830 contains the cursor, or None if there is no such line.
808 """
831 """
809 if self._executing:
832 if self._executing:
810 return None
833 return None
811 cursor = self._control.textCursor()
834 cursor = self._control.textCursor()
812 if cursor.position() >= self._prompt_pos:
835 if cursor.position() >= self._prompt_pos:
813 text = self._get_block_plain_text(cursor.block())
814 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
836 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
815 return text[len(self._prompt):]
837 return self._prompt
816 else:
838 else:
817 return text[len(self._continuation_prompt):]
839 return self._continuation_prompt
818 else:
840 else:
819 return None
841 return None
820
842
821 def _get_prompt_cursor(self):
843 def _get_prompt_cursor(self):
822 """ Convenience method that returns a cursor for the prompt position.
844 """ Convenience method that returns a cursor for the prompt position.
823 """
845 """
824 cursor = self._control.textCursor()
846 cursor = self._control.textCursor()
825 cursor.setPosition(self._prompt_pos)
847 cursor.setPosition(self._prompt_pos)
826 return cursor
848 return cursor
827
849
828 def _get_selection_cursor(self, start, end):
850 def _get_selection_cursor(self, start, end):
829 """ Convenience method that returns a cursor with text selected between
851 """ Convenience method that returns a cursor with text selected between
830 the positions 'start' and 'end'.
852 the positions 'start' and 'end'.
831 """
853 """
832 cursor = self._control.textCursor()
854 cursor = self._control.textCursor()
833 cursor.setPosition(start)
855 cursor.setPosition(start)
834 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
856 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
835 return cursor
857 return cursor
836
858
837 def _get_word_start_cursor(self, position):
859 def _get_word_start_cursor(self, position):
838 """ Find the start of the word to the left the given position. If a
860 """ Find the start of the word to the left the given position. If a
839 sequence of non-word characters precedes the first word, skip over
861 sequence of non-word characters precedes the first word, skip over
840 them. (This emulates the behavior of bash, emacs, etc.)
862 them. (This emulates the behavior of bash, emacs, etc.)
841 """
863 """
842 document = self._control.document()
864 document = self._control.document()
843 position -= 1
865 position -= 1
844 while position >= self._prompt_pos and \
866 while position >= self._prompt_pos and \
845 not document.characterAt(position).isLetterOrNumber():
867 not document.characterAt(position).isLetterOrNumber():
846 position -= 1
868 position -= 1
847 while position >= self._prompt_pos and \
869 while position >= self._prompt_pos and \
848 document.characterAt(position).isLetterOrNumber():
870 document.characterAt(position).isLetterOrNumber():
849 position -= 1
871 position -= 1
850 cursor = self._control.textCursor()
872 cursor = self._control.textCursor()
851 cursor.setPosition(position + 1)
873 cursor.setPosition(position + 1)
852 return cursor
874 return cursor
853
875
854 def _get_word_end_cursor(self, position):
876 def _get_word_end_cursor(self, position):
855 """ Find the end of the word to the right the given position. If a
877 """ Find the end of the word to the right the given position. If a
856 sequence of non-word characters precedes the first word, skip over
878 sequence of non-word characters precedes the first word, skip over
857 them. (This emulates the behavior of bash, emacs, etc.)
879 them. (This emulates the behavior of bash, emacs, etc.)
858 """
880 """
859 document = self._control.document()
881 document = self._control.document()
860 end = self._get_end_cursor().position()
882 end = self._get_end_cursor().position()
861 while position < end and \
883 while position < end and \
862 not document.characterAt(position).isLetterOrNumber():
884 not document.characterAt(position).isLetterOrNumber():
863 position += 1
885 position += 1
864 while position < end and \
886 while position < end and \
865 document.characterAt(position).isLetterOrNumber():
887 document.characterAt(position).isLetterOrNumber():
866 position += 1
888 position += 1
867 cursor = self._control.textCursor()
889 cursor = self._control.textCursor()
868 cursor.setPosition(position)
890 cursor.setPosition(position)
869 return cursor
891 return cursor
870
892
871 def _insert_html(self, cursor, html):
893 def _insert_html(self, cursor, html):
872 """ Inserts HTML using the specified cursor in such a way that future
894 """ Inserts HTML using the specified cursor in such a way that future
873 formatting is unaffected.
895 formatting is unaffected.
874 """
896 """
875 cursor.beginEditBlock()
897 cursor.beginEditBlock()
876 cursor.insertHtml(html)
898 cursor.insertHtml(html)
877
899
878 # After inserting HTML, the text document "remembers" it's in "html
900 # After inserting HTML, the text document "remembers" it's in "html
879 # mode", which means that subsequent calls adding plain text will result
901 # mode", which means that subsequent calls adding plain text will result
880 # in unwanted formatting, lost tab characters, etc. The following code
902 # in unwanted formatting, lost tab characters, etc. The following code
881 # hacks around this behavior, which I consider to be a bug in Qt, by
903 # hacks around this behavior, which I consider to be a bug in Qt, by
882 # (crudely) resetting the document's style state.
904 # (crudely) resetting the document's style state.
883 cursor.movePosition(QtGui.QTextCursor.Left,
905 cursor.movePosition(QtGui.QTextCursor.Left,
884 QtGui.QTextCursor.KeepAnchor)
906 QtGui.QTextCursor.KeepAnchor)
885 if cursor.selection().toPlainText() == ' ':
907 if cursor.selection().toPlainText() == ' ':
886 cursor.removeSelectedText()
908 cursor.removeSelectedText()
887 else:
909 else:
888 cursor.movePosition(QtGui.QTextCursor.Right)
910 cursor.movePosition(QtGui.QTextCursor.Right)
889 cursor.insertText(' ', QtGui.QTextCharFormat())
911 cursor.insertText(' ', QtGui.QTextCharFormat())
890 cursor.endEditBlock()
912 cursor.endEditBlock()
891
913
892 def _insert_html_fetching_plain_text(self, cursor, html):
914 def _insert_html_fetching_plain_text(self, cursor, html):
893 """ Inserts HTML using the specified cursor, then returns its plain text
915 """ Inserts HTML using the specified cursor, then returns its plain text
894 version.
916 version.
895 """
917 """
896 cursor.beginEditBlock()
918 cursor.beginEditBlock()
897 cursor.removeSelectedText()
919 cursor.removeSelectedText()
898
920
899 start = cursor.position()
921 start = cursor.position()
900 self._insert_html(cursor, html)
922 self._insert_html(cursor, html)
901 end = cursor.position()
923 end = cursor.position()
902 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
924 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
903 text = str(cursor.selection().toPlainText())
925 text = str(cursor.selection().toPlainText())
904
926
905 cursor.setPosition(end)
927 cursor.setPosition(end)
906 cursor.endEditBlock()
928 cursor.endEditBlock()
907 return text
929 return text
908
930
909 def _insert_plain_text(self, cursor, text):
931 def _insert_plain_text(self, cursor, text):
910 """ Inserts plain text using the specified cursor, processing ANSI codes
932 """ Inserts plain text using the specified cursor, processing ANSI codes
911 if enabled.
933 if enabled.
912 """
934 """
913 cursor.beginEditBlock()
935 cursor.beginEditBlock()
914 if self.ansi_codes:
936 if self.ansi_codes:
915 for substring in self._ansi_processor.split_string(text):
937 for substring in self._ansi_processor.split_string(text):
916 for action in self._ansi_processor.actions:
938 for action in self._ansi_processor.actions:
917 if action.kind == 'erase' and action.area == 'screen':
939 if action.kind == 'erase' and action.area == 'screen':
918 cursor.select(QtGui.QTextCursor.Document)
940 cursor.select(QtGui.QTextCursor.Document)
919 cursor.removeSelectedText()
941 cursor.removeSelectedText()
920 format = self._ansi_processor.get_format()
942 format = self._ansi_processor.get_format()
921 cursor.insertText(substring, format)
943 cursor.insertText(substring, format)
922 else:
944 else:
923 cursor.insertText(text)
945 cursor.insertText(text)
924 cursor.endEditBlock()
946 cursor.endEditBlock()
925
947
926 def _insert_plain_text_into_buffer(self, text):
948 def _insert_plain_text_into_buffer(self, text):
927 """ Inserts text into the input buffer at the current cursor position,
949 """ Inserts text into the input buffer at the current cursor position,
928 ensuring that continuation prompts are inserted as necessary.
950 ensuring that continuation prompts are inserted as necessary.
929 """
951 """
930 lines = str(text).splitlines(True)
952 lines = str(text).splitlines(True)
931 if lines:
953 if lines:
932 self._keep_cursor_in_buffer()
954 self._keep_cursor_in_buffer()
933 cursor = self._control.textCursor()
955 cursor = self._control.textCursor()
934 cursor.beginEditBlock()
956 cursor.beginEditBlock()
935 cursor.insertText(lines[0])
957 cursor.insertText(lines[0])
936 for line in lines[1:]:
958 for line in lines[1:]:
937 if self._continuation_prompt_html is None:
959 if self._continuation_prompt_html is None:
938 cursor.insertText(self._continuation_prompt)
960 cursor.insertText(self._continuation_prompt)
939 else:
961 else:
940 self._continuation_prompt = \
962 self._continuation_prompt = \
941 self._insert_html_fetching_plain_text(
963 self._insert_html_fetching_plain_text(
942 cursor, self._continuation_prompt_html)
964 cursor, self._continuation_prompt_html)
943 cursor.insertText(line)
965 cursor.insertText(line)
944 cursor.endEditBlock()
966 cursor.endEditBlock()
945 self._control.setTextCursor(cursor)
967 self._control.setTextCursor(cursor)
946
968
947 def _in_buffer(self, position):
969 def _in_buffer(self, position):
948 """ Returns whether the given position is inside the editing region.
970 """ Returns whether the given position is inside the editing region.
949 """
971 """
950 cursor = self._control.textCursor()
972 cursor = self._control.textCursor()
951 cursor.setPosition(position)
973 cursor.setPosition(position)
952 line = cursor.blockNumber()
974 line = cursor.blockNumber()
953 prompt_line = self._get_prompt_cursor().blockNumber()
975 prompt_line = self._get_prompt_cursor().blockNumber()
954 if line == prompt_line:
976 if line == prompt_line:
955 return position >= self._prompt_pos
977 return position >= self._prompt_pos
956 elif line > prompt_line:
978 elif line > prompt_line:
957 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
979 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
958 prompt_pos = cursor.position() + len(self._continuation_prompt)
980 prompt_pos = cursor.position() + len(self._continuation_prompt)
959 return position >= prompt_pos
981 return position >= prompt_pos
960 return False
982 return False
961
983
962 def _keep_cursor_in_buffer(self):
984 def _keep_cursor_in_buffer(self):
963 """ Ensures that the cursor is inside the editing region. Returns
985 """ Ensures that the cursor is inside the editing region. Returns
964 whether the cursor was moved.
986 whether the cursor was moved.
965 """
987 """
966 cursor = self._control.textCursor()
988 cursor = self._control.textCursor()
967 if self._in_buffer(cursor.position()):
989 if self._in_buffer(cursor.position()):
968 return False
990 return False
969 else:
991 else:
970 cursor.movePosition(QtGui.QTextCursor.End)
992 cursor.movePosition(QtGui.QTextCursor.End)
971 self._control.setTextCursor(cursor)
993 self._control.setTextCursor(cursor)
972 return True
994 return True
973
995
974 def _page(self, text):
996 def _page(self, text):
975 """ Displays text using the pager.
997 """ Displays text using the pager.
976 """
998 """
977 if self._page_style == 'custom':
999 if self._page_style == 'custom':
978 self.custom_page_requested.emit(text)
1000 self.custom_page_requested.emit(text)
979 elif self._page_style == 'none':
1001 elif self._page_style == 'none':
980 self._append_plain_text(text)
1002 self._append_plain_text(text)
981 else:
1003 else:
982 self._page_control.clear()
1004 self._page_control.clear()
983 cursor = self._page_control.textCursor()
1005 cursor = self._page_control.textCursor()
984 self._insert_plain_text(cursor, text)
1006 self._insert_plain_text(cursor, text)
985 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1007 self._page_control.moveCursor(QtGui.QTextCursor.Start)
986
1008
987 self._page_control.viewport().resize(self._control.size())
1009 self._page_control.viewport().resize(self._control.size())
988 if self._splitter:
1010 if self._splitter:
989 self._page_control.show()
1011 self._page_control.show()
990 self._page_control.setFocus()
1012 self._page_control.setFocus()
991 else:
1013 else:
992 self.layout().setCurrentWidget(self._page_control)
1014 self.layout().setCurrentWidget(self._page_control)
993
1015
994 def _prompt_started(self):
1016 def _prompt_started(self):
995 """ Called immediately after a new prompt is displayed.
1017 """ Called immediately after a new prompt is displayed.
996 """
1018 """
997 # Temporarily disable the maximum block count to permit undo/redo and
1019 # Temporarily disable the maximum block count to permit undo/redo and
998 # to ensure that the prompt position does not change due to truncation.
1020 # to ensure that the prompt position does not change due to truncation.
999 self._control.document().setMaximumBlockCount(0)
1021 self._control.document().setMaximumBlockCount(0)
1000 self._control.setUndoRedoEnabled(True)
1022 self._control.setUndoRedoEnabled(True)
1001
1023
1002 self._control.setReadOnly(False)
1024 self._control.setReadOnly(False)
1003 self._control.moveCursor(QtGui.QTextCursor.End)
1025 self._control.moveCursor(QtGui.QTextCursor.End)
1004
1026
1005 self._executing = False
1027 self._executing = False
1006 self._prompt_started_hook()
1028 self._prompt_started_hook()
1007
1029
1008 def _prompt_finished(self):
1030 def _prompt_finished(self):
1009 """ Called immediately after a prompt is finished, i.e. when some input
1031 """ Called immediately after a prompt is finished, i.e. when some input
1010 will be processed and a new prompt displayed.
1032 will be processed and a new prompt displayed.
1011 """
1033 """
1012 self._control.setUndoRedoEnabled(False)
1034 self._control.setUndoRedoEnabled(False)
1013 self._control.setReadOnly(True)
1035 self._control.setReadOnly(True)
1014 self._prompt_finished_hook()
1036 self._prompt_finished_hook()
1015
1037
1016 def _readline(self, prompt='', callback=None):
1038 def _readline(self, prompt='', callback=None):
1017 """ Reads one line of input from the user.
1039 """ Reads one line of input from the user.
1018
1040
1019 Parameters
1041 Parameters
1020 ----------
1042 ----------
1021 prompt : str, optional
1043 prompt : str, optional
1022 The prompt to print before reading the line.
1044 The prompt to print before reading the line.
1023
1045
1024 callback : callable, optional
1046 callback : callable, optional
1025 A callback to execute with the read line. If not specified, input is
1047 A callback to execute with the read line. If not specified, input is
1026 read *synchronously* and this method does not return until it has
1048 read *synchronously* and this method does not return until it has
1027 been read.
1049 been read.
1028
1050
1029 Returns
1051 Returns
1030 -------
1052 -------
1031 If a callback is specified, returns nothing. Otherwise, returns the
1053 If a callback is specified, returns nothing. Otherwise, returns the
1032 input string with the trailing newline stripped.
1054 input string with the trailing newline stripped.
1033 """
1055 """
1034 if self._reading:
1056 if self._reading:
1035 raise RuntimeError('Cannot read a line. Widget is already reading.')
1057 raise RuntimeError('Cannot read a line. Widget is already reading.')
1036
1058
1037 if not callback and not self.isVisible():
1059 if not callback and not self.isVisible():
1038 # If the user cannot see the widget, this function cannot return.
1060 # If the user cannot see the widget, this function cannot return.
1039 raise RuntimeError('Cannot synchronously read a line if the widget'
1061 raise RuntimeError('Cannot synchronously read a line if the widget'
1040 'is not visible!')
1062 'is not visible!')
1041
1063
1042 self._reading = True
1064 self._reading = True
1043 self._show_prompt(prompt, newline=False)
1065 self._show_prompt(prompt, newline=False)
1044
1066
1045 if callback is None:
1067 if callback is None:
1046 self._reading_callback = None
1068 self._reading_callback = None
1047 while self._reading:
1069 while self._reading:
1048 QtCore.QCoreApplication.processEvents()
1070 QtCore.QCoreApplication.processEvents()
1049 return self.input_buffer.rstrip('\n')
1071 return self.input_buffer.rstrip('\n')
1050
1072
1051 else:
1073 else:
1052 self._reading_callback = lambda: \
1074 self._reading_callback = lambda: \
1053 callback(self.input_buffer.rstrip('\n'))
1075 callback(self.input_buffer.rstrip('\n'))
1054
1076
1055 def _reset(self):
1077 def _reset(self):
1056 """ Clears the console and resets internal state variables.
1078 """ Clears the console and resets internal state variables.
1057 """
1079 """
1058 self._control.clear()
1080 self._control.clear()
1059 self._executing = self._reading = False
1081 self._executing = self._reading = False
1060
1082
1061 def _set_continuation_prompt(self, prompt, html=False):
1083 def _set_continuation_prompt(self, prompt, html=False):
1062 """ Sets the continuation prompt.
1084 """ Sets the continuation prompt.
1063
1085
1064 Parameters
1086 Parameters
1065 ----------
1087 ----------
1066 prompt : str
1088 prompt : str
1067 The prompt to show when more input is needed.
1089 The prompt to show when more input is needed.
1068
1090
1069 html : bool, optional (default False)
1091 html : bool, optional (default False)
1070 If set, the prompt will be inserted as formatted HTML. Otherwise,
1092 If set, the prompt will be inserted as formatted HTML. Otherwise,
1071 the prompt will be treated as plain text, though ANSI color codes
1093 the prompt will be treated as plain text, though ANSI color codes
1072 will be handled.
1094 will be handled.
1073 """
1095 """
1074 if html:
1096 if html:
1075 self._continuation_prompt_html = prompt
1097 self._continuation_prompt_html = prompt
1076 else:
1098 else:
1077 self._continuation_prompt = prompt
1099 self._continuation_prompt = prompt
1078 self._continuation_prompt_html = None
1100 self._continuation_prompt_html = None
1079
1101
1080 def _set_cursor(self, cursor):
1102 def _set_cursor(self, cursor):
1081 """ Convenience method to set the current cursor.
1103 """ Convenience method to set the current cursor.
1082 """
1104 """
1083 self._control.setTextCursor(cursor)
1105 self._control.setTextCursor(cursor)
1084
1106
1085 def _set_position(self, position):
1107 def _set_position(self, position):
1086 """ Convenience method to set the position of the cursor.
1108 """ Convenience method to set the position of the cursor.
1087 """
1109 """
1088 cursor = self._control.textCursor()
1110 cursor = self._control.textCursor()
1089 cursor.setPosition(position)
1111 cursor.setPosition(position)
1090 self._control.setTextCursor(cursor)
1112 self._control.setTextCursor(cursor)
1091
1113
1092 def _set_selection(self, start, end):
1114 def _set_selection(self, start, end):
1093 """ Convenience method to set the current selected text.
1115 """ Convenience method to set the current selected text.
1094 """
1116 """
1095 self._control.setTextCursor(self._get_selection_cursor(start, end))
1117 self._control.setTextCursor(self._get_selection_cursor(start, end))
1096
1118
1097 def _show_context_menu(self, pos):
1119 def _show_context_menu(self, pos):
1098 """ Shows a context menu at the given QPoint (in widget coordinates).
1120 """ Shows a context menu at the given QPoint (in widget coordinates).
1099 """
1121 """
1100 menu = QtGui.QMenu()
1122 menu = QtGui.QMenu()
1101
1123
1102 copy_action = menu.addAction('Copy', self.copy)
1124 copy_action = menu.addAction('Copy', self.copy)
1103 copy_action.setEnabled(self._get_cursor().hasSelection())
1125 copy_action.setEnabled(self._get_cursor().hasSelection())
1104 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1126 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1105
1127
1106 paste_action = menu.addAction('Paste', self.paste)
1128 paste_action = menu.addAction('Paste', self.paste)
1107 paste_action.setEnabled(self.can_paste())
1129 paste_action.setEnabled(self.can_paste())
1108 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1130 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1109
1131
1110 menu.addSeparator()
1132 menu.addSeparator()
1111 menu.addAction('Select All', self.select_all)
1133 menu.addAction('Select All', self.select_all)
1112
1134
1113 menu.exec_(self._control.mapToGlobal(pos))
1135 menu.exec_(self._control.mapToGlobal(pos))
1114
1136
1115 def _show_prompt(self, prompt=None, html=False, newline=True):
1137 def _show_prompt(self, prompt=None, html=False, newline=True):
1116 """ Writes a new prompt at the end of the buffer.
1138 """ Writes a new prompt at the end of the buffer.
1117
1139
1118 Parameters
1140 Parameters
1119 ----------
1141 ----------
1120 prompt : str, optional
1142 prompt : str, optional
1121 The prompt to show. If not specified, the previous prompt is used.
1143 The prompt to show. If not specified, the previous prompt is used.
1122
1144
1123 html : bool, optional (default False)
1145 html : bool, optional (default False)
1124 Only relevant when a prompt is specified. If set, the prompt will
1146 Only relevant when a prompt is specified. If set, the prompt will
1125 be inserted as formatted HTML. Otherwise, the prompt will be treated
1147 be inserted as formatted HTML. Otherwise, the prompt will be treated
1126 as plain text, though ANSI color codes will be handled.
1148 as plain text, though ANSI color codes will be handled.
1127
1149
1128 newline : bool, optional (default True)
1150 newline : bool, optional (default True)
1129 If set, a new line will be written before showing the prompt if
1151 If set, a new line will be written before showing the prompt if
1130 there is not already a newline at the end of the buffer.
1152 there is not already a newline at the end of the buffer.
1131 """
1153 """
1132 # Insert a preliminary newline, if necessary.
1154 # Insert a preliminary newline, if necessary.
1133 if newline:
1155 if newline:
1134 cursor = self._get_end_cursor()
1156 cursor = self._get_end_cursor()
1135 if cursor.position() > 0:
1157 if cursor.position() > 0:
1136 cursor.movePosition(QtGui.QTextCursor.Left,
1158 cursor.movePosition(QtGui.QTextCursor.Left,
1137 QtGui.QTextCursor.KeepAnchor)
1159 QtGui.QTextCursor.KeepAnchor)
1138 if str(cursor.selection().toPlainText()) != '\n':
1160 if str(cursor.selection().toPlainText()) != '\n':
1139 self._append_plain_text('\n')
1161 self._append_plain_text('\n')
1140
1162
1141 # Write the prompt.
1163 # Write the prompt.
1142 if prompt is None:
1164 if prompt is None:
1143 if self._prompt_html is None:
1165 if self._prompt_html is None:
1144 self._append_plain_text(self._prompt)
1166 self._append_plain_text(self._prompt)
1145 else:
1167 else:
1146 self._append_html(self._prompt_html)
1168 self._append_html(self._prompt_html)
1147 else:
1169 else:
1148 if html:
1170 if html:
1149 self._prompt = self._append_html_fetching_plain_text(prompt)
1171 self._prompt = self._append_html_fetching_plain_text(prompt)
1150 self._prompt_html = prompt
1172 self._prompt_html = prompt
1151 else:
1173 else:
1152 self._append_plain_text(prompt)
1174 self._append_plain_text(prompt)
1153 self._prompt = prompt
1175 self._prompt = prompt
1154 self._prompt_html = None
1176 self._prompt_html = None
1155
1177
1156 self._prompt_pos = self._get_end_cursor().position()
1178 self._prompt_pos = self._get_end_cursor().position()
1157 self._prompt_started()
1179 self._prompt_started()
1158
1180
1159 def _show_continuation_prompt(self):
1181 def _show_continuation_prompt(self):
1160 """ Writes a new continuation prompt at the end of the buffer.
1182 """ Writes a new continuation prompt at the end of the buffer.
1161 """
1183 """
1162 if self._continuation_prompt_html is None:
1184 if self._continuation_prompt_html is None:
1163 self._append_plain_text(self._continuation_prompt)
1185 self._append_plain_text(self._continuation_prompt)
1164 else:
1186 else:
1165 self._continuation_prompt = self._append_html_fetching_plain_text(
1187 self._continuation_prompt = self._append_html_fetching_plain_text(
1166 self._continuation_prompt_html)
1188 self._continuation_prompt_html)
1167
1189
1168 self._prompt_started()
1190 self._prompt_started()
1169
1191
1170
1192
1171 class HistoryConsoleWidget(ConsoleWidget):
1193 class HistoryConsoleWidget(ConsoleWidget):
1172 """ A ConsoleWidget that keeps a history of the commands that have been
1194 """ A ConsoleWidget that keeps a history of the commands that have been
1173 executed.
1195 executed.
1174 """
1196 """
1175
1197
1176 #---------------------------------------------------------------------------
1198 #---------------------------------------------------------------------------
1177 # 'object' interface
1199 # 'object' interface
1178 #---------------------------------------------------------------------------
1200 #---------------------------------------------------------------------------
1179
1201
1180 def __init__(self, *args, **kw):
1202 def __init__(self, *args, **kw):
1181 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1203 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1182 self._history = []
1204 self._history = []
1183 self._history_index = 0
1205 self._history_index = 0
1184
1206
1185 #---------------------------------------------------------------------------
1207 #---------------------------------------------------------------------------
1186 # 'ConsoleWidget' public interface
1208 # 'ConsoleWidget' public interface
1187 #---------------------------------------------------------------------------
1209 #---------------------------------------------------------------------------
1188
1210
1189 def execute(self, source=None, hidden=False, interactive=False):
1211 def execute(self, source=None, hidden=False, interactive=False):
1190 """ Reimplemented to the store history.
1212 """ Reimplemented to the store history.
1191 """
1213 """
1192 if not hidden:
1214 if not hidden:
1193 history = self.input_buffer if source is None else source
1215 history = self.input_buffer if source is None else source
1194
1216
1195 executed = super(HistoryConsoleWidget, self).execute(
1217 executed = super(HistoryConsoleWidget, self).execute(
1196 source, hidden, interactive)
1218 source, hidden, interactive)
1197
1219
1198 if executed and not hidden:
1220 if executed and not hidden:
1199 self._history.append(history.rstrip())
1221 self._history.append(history.rstrip())
1200 self._history_index = len(self._history)
1222 self._history_index = len(self._history)
1201
1223
1202 return executed
1224 return executed
1203
1225
1204 #---------------------------------------------------------------------------
1226 #---------------------------------------------------------------------------
1205 # 'ConsoleWidget' abstract interface
1227 # 'ConsoleWidget' abstract interface
1206 #---------------------------------------------------------------------------
1228 #---------------------------------------------------------------------------
1207
1229
1208 def _up_pressed(self):
1230 def _up_pressed(self):
1209 """ Called when the up key is pressed. Returns whether to continue
1231 """ Called when the up key is pressed. Returns whether to continue
1210 processing the event.
1232 processing the event.
1211 """
1233 """
1212 prompt_cursor = self._get_prompt_cursor()
1234 prompt_cursor = self._get_prompt_cursor()
1213 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1235 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1214 self.history_previous()
1236 self.history_previous()
1215
1237
1216 # Go to the first line of prompt for seemless history scrolling.
1238 # Go to the first line of prompt for seemless history scrolling.
1217 cursor = self._get_prompt_cursor()
1239 cursor = self._get_prompt_cursor()
1218 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1240 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1219 self._set_cursor(cursor)
1241 self._set_cursor(cursor)
1220
1242
1221 return False
1243 return False
1222 return True
1244 return True
1223
1245
1224 def _down_pressed(self):
1246 def _down_pressed(self):
1225 """ Called when the down key is pressed. Returns whether to continue
1247 """ Called when the down key is pressed. Returns whether to continue
1226 processing the event.
1248 processing the event.
1227 """
1249 """
1228 end_cursor = self._get_end_cursor()
1250 end_cursor = self._get_end_cursor()
1229 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1251 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1230 self.history_next()
1252 self.history_next()
1231 return False
1253 return False
1232 return True
1254 return True
1233
1255
1234 #---------------------------------------------------------------------------
1256 #---------------------------------------------------------------------------
1235 # 'HistoryConsoleWidget' interface
1257 # 'HistoryConsoleWidget' interface
1236 #---------------------------------------------------------------------------
1258 #---------------------------------------------------------------------------
1237
1259
1238 def history_previous(self):
1260 def history_previous(self):
1239 """ If possible, set the input buffer to the previous item in the
1261 """ If possible, set the input buffer to the previous item in the
1240 history.
1262 history.
1241 """
1263 """
1242 if self._history_index > 0:
1264 if self._history_index > 0:
1243 self._history_index -= 1
1265 self._history_index -= 1
1244 self.input_buffer = self._history[self._history_index]
1266 self.input_buffer = self._history[self._history_index]
1245
1267
1246 def history_next(self):
1268 def history_next(self):
1247 """ Set the input buffer to the next item in the history, or a blank
1269 """ Set the input buffer to the next item in the history, or a blank
1248 line if there is no subsequent item.
1270 line if there is no subsequent item.
1249 """
1271 """
1250 if self._history_index < len(self._history):
1272 if self._history_index < len(self._history):
1251 self._history_index += 1
1273 self._history_index += 1
1252 if self._history_index < len(self._history):
1274 if self._history_index < len(self._history):
1253 self.input_buffer = self._history[self._history_index]
1275 self.input_buffer = self._history[self._history_index]
1254 else:
1276 else:
1255 self.input_buffer = ''
1277 self.input_buffer = ''
@@ -1,384 +1,379 b''
1 # Standard library imports
1 # Standard library imports
2 import signal
2 import signal
3 import sys
3 import sys
4
4
5 # System library imports
5 # System library imports
6 from pygments.lexers import PythonLexer
6 from pygments.lexers import PythonLexer
7 from PyQt4 import QtCore, QtGui
7 from PyQt4 import QtCore, QtGui
8 import zmq
8 import zmq
9
9
10 # Local imports
10 # Local imports
11 from IPython.core.inputsplitter import InputSplitter
11 from IPython.core.inputsplitter import InputSplitter
12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
13 from call_tip_widget import CallTipWidget
13 from call_tip_widget import CallTipWidget
14 from completion_lexer import CompletionLexer
14 from completion_lexer import CompletionLexer
15 from console_widget import HistoryConsoleWidget
15 from console_widget import HistoryConsoleWidget
16 from pygments_highlighter import PygmentsHighlighter
16 from pygments_highlighter import PygmentsHighlighter
17
17
18
18
19 class FrontendHighlighter(PygmentsHighlighter):
19 class FrontendHighlighter(PygmentsHighlighter):
20 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 """ A PygmentsHighlighter that can be turned on and off and that ignores
21 prompts.
21 prompts.
22 """
22 """
23
23
24 def __init__(self, frontend):
24 def __init__(self, frontend):
25 super(FrontendHighlighter, self).__init__(frontend._control.document())
25 super(FrontendHighlighter, self).__init__(frontend._control.document())
26 self._current_offset = 0
26 self._current_offset = 0
27 self._frontend = frontend
27 self._frontend = frontend
28 self.highlighting_on = False
28 self.highlighting_on = False
29
29
30 def highlightBlock(self, qstring):
30 def highlightBlock(self, qstring):
31 """ Highlight a block of text. Reimplemented to highlight selectively.
31 """ Highlight a block of text. Reimplemented to highlight selectively.
32 """
32 """
33 if not self.highlighting_on:
33 if not self.highlighting_on:
34 return
34 return
35
35
36 # The input to this function is unicode string that may contain
36 # The input to this function is unicode string that may contain
37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
38 # the string as plain text so we can compare it.
38 # the string as plain text so we can compare it.
39 current_block = self.currentBlock()
39 current_block = self.currentBlock()
40 string = self._frontend._get_block_plain_text(current_block)
40 string = self._frontend._get_block_plain_text(current_block)
41
41
42 # Decide whether to check for the regular or continuation prompt.
42 # Decide whether to check for the regular or continuation prompt.
43 if current_block.contains(self._frontend._prompt_pos):
43 if current_block.contains(self._frontend._prompt_pos):
44 prompt = self._frontend._prompt
44 prompt = self._frontend._prompt
45 else:
45 else:
46 prompt = self._frontend._continuation_prompt
46 prompt = self._frontend._continuation_prompt
47
47
48 # Don't highlight the part of the string that contains the prompt.
48 # Don't highlight the part of the string that contains the prompt.
49 if string.startswith(prompt):
49 if string.startswith(prompt):
50 self._current_offset = len(prompt)
50 self._current_offset = len(prompt)
51 qstring.remove(0, len(prompt))
51 qstring.remove(0, len(prompt))
52 else:
52 else:
53 self._current_offset = 0
53 self._current_offset = 0
54
54
55 PygmentsHighlighter.highlightBlock(self, qstring)
55 PygmentsHighlighter.highlightBlock(self, qstring)
56
56
57 def rehighlightBlock(self, block):
57 def rehighlightBlock(self, block):
58 """ Reimplemented to temporarily enable highlighting if disabled.
58 """ Reimplemented to temporarily enable highlighting if disabled.
59 """
59 """
60 old = self.highlighting_on
60 old = self.highlighting_on
61 self.highlighting_on = True
61 self.highlighting_on = True
62 super(FrontendHighlighter, self).rehighlightBlock(block)
62 super(FrontendHighlighter, self).rehighlightBlock(block)
63 self.highlighting_on = old
63 self.highlighting_on = old
64
64
65 def setFormat(self, start, count, format):
65 def setFormat(self, start, count, format):
66 """ Reimplemented to highlight selectively.
66 """ Reimplemented to highlight selectively.
67 """
67 """
68 start += self._current_offset
68 start += self._current_offset
69 PygmentsHighlighter.setFormat(self, start, count, format)
69 PygmentsHighlighter.setFormat(self, start, count, format)
70
70
71
71
72 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
72 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
73 """ A Qt frontend for a generic Python kernel.
73 """ A Qt frontend for a generic Python kernel.
74 """
74 """
75
75
76 # Emitted when an 'execute_reply' has been received from the kernel and
76 # Emitted when an 'execute_reply' has been received from the kernel and
77 # processed by the FrontendWidget.
77 # processed by the FrontendWidget.
78 executed = QtCore.pyqtSignal(object)
78 executed = QtCore.pyqtSignal(object)
79
79
80 # Protected class attributes.
80 # Protected class attributes.
81 _highlighter_class = FrontendHighlighter
81 _highlighter_class = FrontendHighlighter
82 _input_splitter_class = InputSplitter
82 _input_splitter_class = InputSplitter
83
83
84 #---------------------------------------------------------------------------
84 #---------------------------------------------------------------------------
85 # 'object' interface
85 # 'object' interface
86 #---------------------------------------------------------------------------
86 #---------------------------------------------------------------------------
87
87
88 def __init__(self, *args, **kw):
88 def __init__(self, *args, **kw):
89 super(FrontendWidget, self).__init__(*args, **kw)
89 super(FrontendWidget, self).__init__(*args, **kw)
90
90
91 # FrontendWidget protected variables.
91 # FrontendWidget protected variables.
92 self._call_tip_widget = CallTipWidget(self._control)
92 self._call_tip_widget = CallTipWidget(self._control)
93 self._completion_lexer = CompletionLexer(PythonLexer())
93 self._completion_lexer = CompletionLexer(PythonLexer())
94 self._hidden = False
94 self._hidden = False
95 self._highlighter = self._highlighter_class(self)
95 self._highlighter = self._highlighter_class(self)
96 self._input_splitter = self._input_splitter_class(input_mode='replace')
96 self._input_splitter = self._input_splitter_class(input_mode='replace')
97 self._kernel_manager = None
97 self._kernel_manager = None
98
98
99 # Configure the ConsoleWidget.
99 # Configure the ConsoleWidget.
100 self.tab_width = 4
100 self.tab_width = 4
101 self._set_continuation_prompt('... ')
101 self._set_continuation_prompt('... ')
102
102
103 # Connect signal handlers.
103 # Connect signal handlers.
104 document = self._control.document()
104 document = self._control.document()
105 document.contentsChange.connect(self._document_contents_change)
105 document.contentsChange.connect(self._document_contents_change)
106
106
107 #---------------------------------------------------------------------------
107 #---------------------------------------------------------------------------
108 # 'ConsoleWidget' abstract interface
108 # 'ConsoleWidget' abstract interface
109 #---------------------------------------------------------------------------
109 #---------------------------------------------------------------------------
110
110
111 def _is_complete(self, source, interactive):
111 def _is_complete(self, source, interactive):
112 """ Returns whether 'source' can be completely processed and a new
112 """ Returns whether 'source' can be completely processed and a new
113 prompt created. When triggered by an Enter/Return key press,
113 prompt created. When triggered by an Enter/Return key press,
114 'interactive' is True; otherwise, it is False.
114 'interactive' is True; otherwise, it is False.
115 """
115 """
116 complete = self._input_splitter.push(source.expandtabs(4))
116 complete = self._input_splitter.push(source.expandtabs(4))
117 if interactive:
117 if interactive:
118 complete = not self._input_splitter.push_accepts_more()
118 complete = not self._input_splitter.push_accepts_more()
119 return complete
119 return complete
120
120
121 def _execute(self, source, hidden):
121 def _execute(self, source, hidden):
122 """ Execute 'source'. If 'hidden', do not show any output.
122 """ Execute 'source'. If 'hidden', do not show any output.
123 """
123 """
124 self.kernel_manager.xreq_channel.execute(source)
124 self.kernel_manager.xreq_channel.execute(source)
125 self._hidden = hidden
125 self._hidden = hidden
126
126
127 def _execute_interrupt(self):
127 def _execute_interrupt(self):
128 """ Attempts to stop execution. Returns whether this method has an
128 """ Attempts to stop execution. Returns whether this method has an
129 implementation.
129 implementation.
130 """
130 """
131 self._interrupt_kernel()
131 self._interrupt_kernel()
132 return True
132 return True
133
133
134 def _prompt_started_hook(self):
134 def _prompt_started_hook(self):
135 """ Called immediately after a new prompt is displayed.
135 """ Called immediately after a new prompt is displayed.
136 """
136 """
137 if not self._reading:
137 if not self._reading:
138 self._highlighter.highlighting_on = True
138 self._highlighter.highlighting_on = True
139
139
140 def _prompt_finished_hook(self):
140 def _prompt_finished_hook(self):
141 """ Called immediately after a prompt is finished, i.e. when some input
141 """ Called immediately after a prompt is finished, i.e. when some input
142 will be processed and a new prompt displayed.
142 will be processed and a new prompt displayed.
143 """
143 """
144 if not self._reading:
144 if not self._reading:
145 self._highlighter.highlighting_on = False
145 self._highlighter.highlighting_on = False
146
146
147 def _tab_pressed(self):
147 def _tab_pressed(self):
148 """ Called when the tab key is pressed. Returns whether to continue
148 """ Called when the tab key is pressed. Returns whether to continue
149 processing the event.
149 processing the event.
150 """
150 """
151 self._keep_cursor_in_buffer()
151 self._keep_cursor_in_buffer()
152 cursor = self._get_cursor()
152 cursor = self._get_cursor()
153 return not self._complete()
153 return not self._complete()
154
154
155 #---------------------------------------------------------------------------
155 #---------------------------------------------------------------------------
156 # 'ConsoleWidget' protected interface
156 # 'ConsoleWidget' protected interface
157 #---------------------------------------------------------------------------
157 #---------------------------------------------------------------------------
158
158
159 def _show_continuation_prompt(self):
159 def _show_continuation_prompt(self):
160 """ Reimplemented for auto-indentation.
160 """ Reimplemented for auto-indentation.
161 """
161 """
162 super(FrontendWidget, self)._show_continuation_prompt()
162 super(FrontendWidget, self)._show_continuation_prompt()
163 spaces = self._input_splitter.indent_spaces
163 spaces = self._input_splitter.indent_spaces
164 self._append_plain_text('\t' * (spaces / self.tab_width))
164 self._append_plain_text('\t' * (spaces / self.tab_width))
165 self._append_plain_text(' ' * (spaces % self.tab_width))
165 self._append_plain_text(' ' * (spaces % self.tab_width))
166
166
167 #---------------------------------------------------------------------------
167 #---------------------------------------------------------------------------
168 # 'BaseFrontendMixin' abstract interface
168 # 'BaseFrontendMixin' abstract interface
169 #---------------------------------------------------------------------------
169 #---------------------------------------------------------------------------
170
170
171 def _handle_complete_reply(self, rep):
171 def _handle_complete_reply(self, rep):
172 """ Handle replies for tab completion.
172 """ Handle replies for tab completion.
173 """
173 """
174 cursor = self._get_cursor()
174 cursor = self._get_cursor()
175 if rep['parent_header']['msg_id'] == self._complete_id and \
175 if rep['parent_header']['msg_id'] == self._complete_id and \
176 cursor.position() == self._complete_pos:
176 cursor.position() == self._complete_pos:
177 text = '.'.join(self._get_context())
177 text = '.'.join(self._get_context())
178 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
178 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
179 self._complete_with_items(cursor, rep['content']['matches'])
179 self._complete_with_items(cursor, rep['content']['matches'])
180
180
181 def _handle_execute_reply(self, msg):
181 def _handle_execute_reply(self, msg):
182 """ Handles replies for code execution.
182 """ Handles replies for code execution.
183 """
183 """
184 if not self._hidden:
184 if not self._hidden:
185 # Make sure that all output from the SUB channel has been processed
185 # Make sure that all output from the SUB channel has been processed
186 # before writing a new prompt.
186 # before writing a new prompt.
187 self.kernel_manager.sub_channel.flush()
187 self.kernel_manager.sub_channel.flush()
188
188
189 content = msg['content']
189 content = msg['content']
190 status = content['status']
190 status = content['status']
191 if status == 'ok':
191 if status == 'ok':
192 self._process_execute_ok(msg)
192 self._process_execute_ok(msg)
193 elif status == 'error':
193 elif status == 'error':
194 self._process_execute_error(msg)
194 self._process_execute_error(msg)
195 elif status == 'abort':
195 elif status == 'abort':
196 self._process_execute_abort(msg)
196 self._process_execute_abort(msg)
197
197
198 self._show_interpreter_prompt_for_reply(msg)
198 self._show_interpreter_prompt_for_reply(msg)
199 self.executed.emit(msg)
199 self.executed.emit(msg)
200
200
201 def _handle_input_request(self, msg):
201 def _handle_input_request(self, msg):
202 """ Handle requests for raw_input.
202 """ Handle requests for raw_input.
203 """
203 """
204 if self._hidden:
204 if self._hidden:
205 raise RuntimeError('Request for raw input during hidden execution.')
205 raise RuntimeError('Request for raw input during hidden execution.')
206
206
207 # Make sure that all output from the SUB channel has been processed
207 # Make sure that all output from the SUB channel has been processed
208 # before entering readline mode.
208 # before entering readline mode.
209 self.kernel_manager.sub_channel.flush()
209 self.kernel_manager.sub_channel.flush()
210
210
211 def callback(line):
211 def callback(line):
212 self.kernel_manager.rep_channel.input(line)
212 self.kernel_manager.rep_channel.input(line)
213 self._readline(msg['content']['prompt'], callback=callback)
213 self._readline(msg['content']['prompt'], callback=callback)
214
214
215 def _handle_object_info_reply(self, rep):
215 def _handle_object_info_reply(self, rep):
216 """ Handle replies for call tips.
216 """ Handle replies for call tips.
217 """
217 """
218 cursor = self._get_cursor()
218 cursor = self._get_cursor()
219 if rep['parent_header']['msg_id'] == self._call_tip_id and \
219 if rep['parent_header']['msg_id'] == self._call_tip_id and \
220 cursor.position() == self._call_tip_pos:
220 cursor.position() == self._call_tip_pos:
221 doc = rep['content']['docstring']
221 doc = rep['content']['docstring']
222 if doc:
222 if doc:
223 self._call_tip_widget.show_docstring(doc)
223 self._call_tip_widget.show_docstring(doc)
224
224
225 def _handle_pyout(self, msg):
225 def _handle_pyout(self, msg):
226 """ Handle display hook output.
226 """ Handle display hook output.
227 """
227 """
228 if not self._hidden and self._is_from_this_session(msg):
228 if not self._hidden and self._is_from_this_session(msg):
229 self._append_plain_text(msg['content']['data'] + '\n')
229 self._append_plain_text(msg['content']['data'] + '\n')
230
230
231 def _handle_stream(self, msg):
231 def _handle_stream(self, msg):
232 """ Handle stdout, stderr, and stdin.
232 """ Handle stdout, stderr, and stdin.
233 """
233 """
234 if not self._hidden and self._is_from_this_session(msg):
234 if not self._hidden and self._is_from_this_session(msg):
235 self._append_plain_text(msg['content']['data'])
235 self._append_plain_text(msg['content']['data'])
236 self._control.moveCursor(QtGui.QTextCursor.End)
236 self._control.moveCursor(QtGui.QTextCursor.End)
237
237
238 def _started_channels(self):
238 def _started_channels(self):
239 """ Called when the KernelManager channels have started listening or
239 """ Called when the KernelManager channels have started listening or
240 when the frontend is assigned an already listening KernelManager.
240 when the frontend is assigned an already listening KernelManager.
241 """
241 """
242 self._reset()
242 self._reset()
243 self._append_plain_text(self._get_banner())
243 self._append_plain_text(self._get_banner())
244 self._show_interpreter_prompt()
244 self._show_interpreter_prompt()
245
245
246 def _stopped_channels(self):
246 def _stopped_channels(self):
247 """ Called when the KernelManager channels have stopped listening or
247 """ Called when the KernelManager channels have stopped listening or
248 when a listening KernelManager is removed from the frontend.
248 when a listening KernelManager is removed from the frontend.
249 """
249 """
250 # FIXME: Print a message here?
250 # FIXME: Print a message here?
251 pass
251 pass
252
252
253 #---------------------------------------------------------------------------
253 #---------------------------------------------------------------------------
254 # 'FrontendWidget' interface
254 # 'FrontendWidget' interface
255 #---------------------------------------------------------------------------
255 #---------------------------------------------------------------------------
256
256
257 def execute_file(self, path, hidden=False):
257 def execute_file(self, path, hidden=False):
258 """ Attempts to execute file with 'path'. If 'hidden', no output is
258 """ Attempts to execute file with 'path'. If 'hidden', no output is
259 shown.
259 shown.
260 """
260 """
261 self.execute('execfile("%s")' % path, hidden=hidden)
261 self.execute('execfile("%s")' % path, hidden=hidden)
262
262
263 #---------------------------------------------------------------------------
263 #---------------------------------------------------------------------------
264 # 'FrontendWidget' protected interface
264 # 'FrontendWidget' protected interface
265 #---------------------------------------------------------------------------
265 #---------------------------------------------------------------------------
266
266
267 def _call_tip(self):
267 def _call_tip(self):
268 """ Shows a call tip, if appropriate, at the current cursor location.
268 """ Shows a call tip, if appropriate, at the current cursor location.
269 """
269 """
270 # Decide if it makes sense to show a call tip
270 # Decide if it makes sense to show a call tip
271 cursor = self._get_cursor()
271 cursor = self._get_cursor()
272 cursor.movePosition(QtGui.QTextCursor.Left)
272 cursor.movePosition(QtGui.QTextCursor.Left)
273 document = self._control.document()
273 document = self._control.document()
274 if document.characterAt(cursor.position()).toAscii() != '(':
274 if document.characterAt(cursor.position()).toAscii() != '(':
275 return False
275 return False
276 context = self._get_context(cursor)
276 context = self._get_context(cursor)
277 if not context:
277 if not context:
278 return False
278 return False
279
279
280 # Send the metadata request to the kernel
280 # Send the metadata request to the kernel
281 name = '.'.join(context)
281 name = '.'.join(context)
282 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
282 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
283 self._call_tip_pos = self._get_cursor().position()
283 self._call_tip_pos = self._get_cursor().position()
284 return True
284 return True
285
285
286 def _complete(self):
286 def _complete(self):
287 """ Performs completion at the current cursor location.
287 """ Performs completion at the current cursor location.
288 """
288 """
289 # Decide if it makes sense to do completion
289 # Decide if it makes sense to do completion
290 context = self._get_context()
290 context = self._get_context()
291 if not context:
291 if not context:
292 return False
292 return False
293
293
294 # Send the completion request to the kernel
294 # Send the completion request to the kernel
295 text = '.'.join(context)
296
297 # FIXME - Evan: we need the position of the cursor in the current input
298 # buffer. I tried this line below but the numbers I get are bogus. -
299 # Not sure what to do. fperez.
300 cursor_pos = self._get_cursor().position()
301
302 self._complete_id = self.kernel_manager.xreq_channel.complete(
295 self._complete_id = self.kernel_manager.xreq_channel.complete(
303 text, self._get_input_buffer_cursor_line(), cursor_pos,
296 '.'.join(context), # text
304 self.input_buffer)
297 self._get_input_buffer_cursor_line(), # line
298 self._get_input_buffer_cursor_column(), # cursor_pos
299 self.input_buffer) # block
305 self._complete_pos = self._get_cursor().position()
300 self._complete_pos = self._get_cursor().position()
306 return True
301 return True
307
302
308 def _get_banner(self):
303 def _get_banner(self):
309 """ Gets a banner to display at the beginning of a session.
304 """ Gets a banner to display at the beginning of a session.
310 """
305 """
311 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
306 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
312 '"license" for more information.'
307 '"license" for more information.'
313 return banner % (sys.version, sys.platform)
308 return banner % (sys.version, sys.platform)
314
309
315 def _get_context(self, cursor=None):
310 def _get_context(self, cursor=None):
316 """ Gets the context at the current cursor location.
311 """ Gets the context at the current cursor location.
317 """
312 """
318 if cursor is None:
313 if cursor is None:
319 cursor = self._get_cursor()
314 cursor = self._get_cursor()
320 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
315 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
321 QtGui.QTextCursor.KeepAnchor)
316 QtGui.QTextCursor.KeepAnchor)
322 text = str(cursor.selection().toPlainText())
317 text = str(cursor.selection().toPlainText())
323 return self._completion_lexer.get_context(text)
318 return self._completion_lexer.get_context(text)
324
319
325 def _interrupt_kernel(self):
320 def _interrupt_kernel(self):
326 """ Attempts to the interrupt the kernel.
321 """ Attempts to the interrupt the kernel.
327 """
322 """
328 if self.kernel_manager.has_kernel:
323 if self.kernel_manager.has_kernel:
329 self.kernel_manager.signal_kernel(signal.SIGINT)
324 self.kernel_manager.signal_kernel(signal.SIGINT)
330 else:
325 else:
331 self._append_plain_text('Kernel process is either remote or '
326 self._append_plain_text('Kernel process is either remote or '
332 'unspecified. Cannot interrupt.\n')
327 'unspecified. Cannot interrupt.\n')
333
328
334 def _process_execute_abort(self, msg):
329 def _process_execute_abort(self, msg):
335 """ Process a reply for an aborted execution request.
330 """ Process a reply for an aborted execution request.
336 """
331 """
337 self._append_plain_text("ERROR: execution aborted\n")
332 self._append_plain_text("ERROR: execution aborted\n")
338
333
339 def _process_execute_error(self, msg):
334 def _process_execute_error(self, msg):
340 """ Process a reply for an execution request that resulted in an error.
335 """ Process a reply for an execution request that resulted in an error.
341 """
336 """
342 content = msg['content']
337 content = msg['content']
343 traceback = ''.join(content['traceback'])
338 traceback = ''.join(content['traceback'])
344 self._append_plain_text(traceback)
339 self._append_plain_text(traceback)
345
340
346 def _process_execute_ok(self, msg):
341 def _process_execute_ok(self, msg):
347 """ Process a reply for a successful execution equest.
342 """ Process a reply for a successful execution equest.
348 """
343 """
349 payload = msg['content']['payload']
344 payload = msg['content']['payload']
350 for item in payload:
345 for item in payload:
351 if not self._process_execute_payload(item):
346 if not self._process_execute_payload(item):
352 warning = 'Received unknown payload of type %s\n'
347 warning = 'Received unknown payload of type %s\n'
353 self._append_plain_text(warning % repr(item['source']))
348 self._append_plain_text(warning % repr(item['source']))
354
349
355 def _process_execute_payload(self, item):
350 def _process_execute_payload(self, item):
356 """ Process a single payload item from the list of payload items in an
351 """ Process a single payload item from the list of payload items in an
357 execution reply. Returns whether the payload was handled.
352 execution reply. Returns whether the payload was handled.
358 """
353 """
359 # The basic FrontendWidget doesn't handle payloads, as they are a
354 # The basic FrontendWidget doesn't handle payloads, as they are a
360 # mechanism for going beyond the standard Python interpreter model.
355 # mechanism for going beyond the standard Python interpreter model.
361 return False
356 return False
362
357
363 def _show_interpreter_prompt(self):
358 def _show_interpreter_prompt(self):
364 """ Shows a prompt for the interpreter.
359 """ Shows a prompt for the interpreter.
365 """
360 """
366 self._show_prompt('>>> ')
361 self._show_prompt('>>> ')
367
362
368 def _show_interpreter_prompt_for_reply(self, msg):
363 def _show_interpreter_prompt_for_reply(self, msg):
369 """ Shows a prompt for the interpreter given an 'execute_reply' message.
364 """ Shows a prompt for the interpreter given an 'execute_reply' message.
370 """
365 """
371 self._show_interpreter_prompt()
366 self._show_interpreter_prompt()
372
367
373 #------ Signal handlers ----------------------------------------------------
368 #------ Signal handlers ----------------------------------------------------
374
369
375 def _document_contents_change(self, position, removed, added):
370 def _document_contents_change(self, position, removed, added):
376 """ Called whenever the document's content changes. Display a call tip
371 """ Called whenever the document's content changes. Display a call tip
377 if appropriate.
372 if appropriate.
378 """
373 """
379 # Calculate where the cursor should be *after* the change:
374 # Calculate where the cursor should be *after* the change:
380 position += added
375 position += added
381
376
382 document = self._control.document()
377 document = self._control.document()
383 if position == self._get_cursor().position():
378 if position == self._get_cursor().position():
384 self._call_tip()
379 self._call_tip()
@@ -1,301 +1,299 b''
1 # Standard library imports
1 # Standard library imports
2 from subprocess import Popen
2 from subprocess import Popen
3
3
4 # System library imports
4 # System library imports
5 from PyQt4 import QtCore, QtGui
5 from PyQt4 import QtCore, QtGui
6
6
7 # Local imports
7 # Local imports
8 from IPython.core.inputsplitter import IPythonInputSplitter
8 from IPython.core.inputsplitter import IPythonInputSplitter
9 from IPython.core.usage import default_banner
9 from IPython.core.usage import default_banner
10 from frontend_widget import FrontendWidget
10 from frontend_widget import FrontendWidget
11
11
12
12
13 class IPythonPromptBlock(object):
13 class IPythonPromptBlock(object):
14 """ An internal storage object for IPythonWidget.
14 """ An internal storage object for IPythonWidget.
15 """
15 """
16 def __init__(self, block, length, number):
16 def __init__(self, block, length, number):
17 self.block = block
17 self.block = block
18 self.length = length
18 self.length = length
19 self.number = number
19 self.number = number
20
20
21
21
22 class IPythonWidget(FrontendWidget):
22 class IPythonWidget(FrontendWidget):
23 """ A FrontendWidget for an IPython kernel.
23 """ A FrontendWidget for an IPython kernel.
24 """
24 """
25
25
26 # Signal emitted when an editor is needed for a file and the editor has been
26 # Signal emitted when an editor is needed for a file and the editor has been
27 # specified as 'custom'. See 'set_editor' for more information.
27 # specified as 'custom'. See 'set_editor' for more information.
28 custom_edit_requested = QtCore.pyqtSignal(object, object)
28 custom_edit_requested = QtCore.pyqtSignal(object, object)
29
29
30 # The default stylesheet: black text on a white background.
30 # The default stylesheet: black text on a white background.
31 default_stylesheet = """
31 default_stylesheet = """
32 .error { color: red; }
32 .error { color: red; }
33 .in-prompt { color: navy; }
33 .in-prompt { color: navy; }
34 .in-prompt-number { font-weight: bold; }
34 .in-prompt-number { font-weight: bold; }
35 .out-prompt { color: darkred; }
35 .out-prompt { color: darkred; }
36 .out-prompt-number { font-weight: bold; }
36 .out-prompt-number { font-weight: bold; }
37 """
37 """
38
38
39 # A dark stylesheet: white text on a black background.
39 # A dark stylesheet: white text on a black background.
40 dark_stylesheet = """
40 dark_stylesheet = """
41 QPlainTextEdit, QTextEdit { background-color: black; color: white }
41 QPlainTextEdit, QTextEdit { background-color: black; color: white }
42 QFrame { border: 1px solid grey; }
42 QFrame { border: 1px solid grey; }
43 .error { color: red; }
43 .error { color: red; }
44 .in-prompt { color: lime; }
44 .in-prompt { color: lime; }
45 .in-prompt-number { color: lime; font-weight: bold; }
45 .in-prompt-number { color: lime; font-weight: bold; }
46 .out-prompt { color: red; }
46 .out-prompt { color: red; }
47 .out-prompt-number { color: red; font-weight: bold; }
47 .out-prompt-number { color: red; font-weight: bold; }
48 """
48 """
49
49
50 # Default prompts.
50 # Default prompts.
51 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
51 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
52 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
52 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
53
53
54 # FrontendWidget protected class variables.
54 # FrontendWidget protected class variables.
55 #_input_splitter_class = IPythonInputSplitter
55 #_input_splitter_class = IPythonInputSplitter
56
56
57 # IPythonWidget protected class variables.
57 # IPythonWidget protected class variables.
58 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
58 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
59 _payload_source_page = 'IPython.zmq.page.page'
59 _payload_source_page = 'IPython.zmq.page.page'
60
60
61 #---------------------------------------------------------------------------
61 #---------------------------------------------------------------------------
62 # 'object' interface
62 # 'object' interface
63 #---------------------------------------------------------------------------
63 #---------------------------------------------------------------------------
64
64
65 def __init__(self, *args, **kw):
65 def __init__(self, *args, **kw):
66 super(IPythonWidget, self).__init__(*args, **kw)
66 super(IPythonWidget, self).__init__(*args, **kw)
67
67
68 # IPythonWidget protected variables.
68 # IPythonWidget protected variables.
69 self._previous_prompt_obj = None
69 self._previous_prompt_obj = None
70
70
71 # Set a default editor and stylesheet.
71 # Set a default editor and stylesheet.
72 self.set_editor('default')
72 self.set_editor('default')
73 self.reset_styling()
73 self.reset_styling()
74
74
75 #---------------------------------------------------------------------------
75 #---------------------------------------------------------------------------
76 # 'BaseFrontendMixin' abstract interface
76 # 'BaseFrontendMixin' abstract interface
77 #---------------------------------------------------------------------------
77 #---------------------------------------------------------------------------
78
78
79 def _handle_pyout(self, msg):
79 def _handle_pyout(self, msg):
80 """ Reimplemented for IPython-style "display hook".
80 """ Reimplemented for IPython-style "display hook".
81 """
81 """
82 if not self._hidden and self._is_from_this_session(msg):
82 if not self._hidden and self._is_from_this_session(msg):
83 content = msg['content']
83 content = msg['content']
84 prompt_number = content['prompt_number']
84 prompt_number = content['prompt_number']
85 self._append_plain_text(content['output_sep'])
85 self._append_plain_text(content['output_sep'])
86 self._append_html(self._make_out_prompt(prompt_number))
86 self._append_html(self._make_out_prompt(prompt_number))
87 self._append_plain_text(content['data'] + '\n' +
87 self._append_plain_text(content['data'] + '\n' +
88 content['output_sep2'])
88 content['output_sep2'])
89
89
90 #---------------------------------------------------------------------------
90 #---------------------------------------------------------------------------
91 # 'FrontendWidget' interface
91 # 'FrontendWidget' interface
92 #---------------------------------------------------------------------------
92 #---------------------------------------------------------------------------
93
93
94 def execute_file(self, path, hidden=False):
94 def execute_file(self, path, hidden=False):
95 """ Reimplemented to use the 'run' magic.
95 """ Reimplemented to use the 'run' magic.
96 """
96 """
97 self.execute('%%run %s' % path, hidden=hidden)
97 self.execute('%%run %s' % path, hidden=hidden)
98
98
99 #---------------------------------------------------------------------------
99 #---------------------------------------------------------------------------
100 # 'FrontendWidget' protected interface
100 # 'FrontendWidget' protected interface
101 #---------------------------------------------------------------------------
101 #---------------------------------------------------------------------------
102
102
103 def _get_banner(self):
103 def _get_banner(self):
104 """ Reimplemented to return IPython's default banner.
104 """ Reimplemented to return IPython's default banner.
105 """
105 """
106 return default_banner + '\n'
106 return default_banner + '\n'
107
107
108 def _process_execute_error(self, msg):
108 def _process_execute_error(self, msg):
109 """ Reimplemented for IPython-style traceback formatting.
109 """ Reimplemented for IPython-style traceback formatting.
110 """
110 """
111 content = msg['content']
111 content = msg['content']
112
112 traceback = '\n'.join(content['traceback']) + '\n'
113 traceback = '\n'.join(content['traceback'])
113 if False:
114
114 # FIXME: For now, tracebacks come as plain text, so we can't use
115 if 0:
116 # FIXME: for now, tracebacks come as plain text, so we can't use
117 # the html renderer yet. Once we refactor ultratb to produce
115 # the html renderer yet. Once we refactor ultratb to produce
118 # properly styled tracebacks, this branch should be the default
116 # properly styled tracebacks, this branch should be the default
119 traceback = traceback.replace(' ', '&nbsp;')
117 traceback = traceback.replace(' ', '&nbsp;')
120 traceback = traceback.replace('\n', '<br/>')
118 traceback = traceback.replace('\n', '<br/>')
121
119
122 ename = content['ename']
120 ename = content['ename']
123 ename_styled = '<span class="error">%s</span>' % ename
121 ename_styled = '<span class="error">%s</span>' % ename
124 traceback = traceback.replace(ename, ename_styled)
122 traceback = traceback.replace(ename, ename_styled)
125
123
126 self._append_html(traceback)
124 self._append_html(traceback)
127 else:
125 else:
128 # This is the fallback for now, using plain text with ansi escapes
126 # This is the fallback for now, using plain text with ansi escapes
129 self._append_plain_text(traceback)
127 self._append_plain_text(traceback)
130
128
131 def _process_execute_payload(self, item):
129 def _process_execute_payload(self, item):
132 """ Reimplemented to handle %edit and paging payloads.
130 """ Reimplemented to handle %edit and paging payloads.
133 """
131 """
134 if item['source'] == self._payload_source_edit:
132 if item['source'] == self._payload_source_edit:
135 self.edit(item['filename'], item['line_number'])
133 self.edit(item['filename'], item['line_number'])
136 return True
134 return True
137 elif item['source'] == self._payload_source_page:
135 elif item['source'] == self._payload_source_page:
138 self._page(item['data'])
136 self._page(item['data'])
139 return True
137 return True
140 else:
138 else:
141 return False
139 return False
142
140
143 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
141 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
144 """ Reimplemented for IPython-style prompts.
142 """ Reimplemented for IPython-style prompts.
145 """
143 """
146 # TODO: If a number was not specified, make a prompt number request.
144 # TODO: If a number was not specified, make a prompt number request.
147 if number is None:
145 if number is None:
148 number = 0
146 number = 0
149
147
150 # Show a new prompt and save information about it so that it can be
148 # Show a new prompt and save information about it so that it can be
151 # updated later if the prompt number turns out to be wrong.
149 # updated later if the prompt number turns out to be wrong.
152 self._append_plain_text(input_sep)
150 self._append_plain_text(input_sep)
153 self._show_prompt(self._make_in_prompt(number), html=True)
151 self._show_prompt(self._make_in_prompt(number), html=True)
154 block = self._control.document().lastBlock()
152 block = self._control.document().lastBlock()
155 length = len(self._prompt)
153 length = len(self._prompt)
156 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
154 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
157
155
158 # Update continuation prompt to reflect (possibly) new prompt length.
156 # Update continuation prompt to reflect (possibly) new prompt length.
159 self._set_continuation_prompt(
157 self._set_continuation_prompt(
160 self._make_continuation_prompt(self._prompt), html=True)
158 self._make_continuation_prompt(self._prompt), html=True)
161
159
162 def _show_interpreter_prompt_for_reply(self, msg):
160 def _show_interpreter_prompt_for_reply(self, msg):
163 """ Reimplemented for IPython-style prompts.
161 """ Reimplemented for IPython-style prompts.
164 """
162 """
165 # Update the old prompt number if necessary.
163 # Update the old prompt number if necessary.
166 content = msg['content']
164 content = msg['content']
167 previous_prompt_number = content['prompt_number']
165 previous_prompt_number = content['prompt_number']
168 if self._previous_prompt_obj and \
166 if self._previous_prompt_obj and \
169 self._previous_prompt_obj.number != previous_prompt_number:
167 self._previous_prompt_obj.number != previous_prompt_number:
170 block = self._previous_prompt_obj.block
168 block = self._previous_prompt_obj.block
171 if block.isValid():
169 if block.isValid():
172
170
173 # Remove the old prompt and insert a new prompt.
171 # Remove the old prompt and insert a new prompt.
174 cursor = QtGui.QTextCursor(block)
172 cursor = QtGui.QTextCursor(block)
175 cursor.movePosition(QtGui.QTextCursor.Right,
173 cursor.movePosition(QtGui.QTextCursor.Right,
176 QtGui.QTextCursor.KeepAnchor,
174 QtGui.QTextCursor.KeepAnchor,
177 self._previous_prompt_obj.length)
175 self._previous_prompt_obj.length)
178 prompt = self._make_in_prompt(previous_prompt_number)
176 prompt = self._make_in_prompt(previous_prompt_number)
179 self._prompt = self._insert_html_fetching_plain_text(
177 self._prompt = self._insert_html_fetching_plain_text(
180 cursor, prompt)
178 cursor, prompt)
181
179
182 # When the HTML is inserted, Qt blows away the syntax
180 # When the HTML is inserted, Qt blows away the syntax
183 # highlighting for the line, so we need to rehighlight it.
181 # highlighting for the line, so we need to rehighlight it.
184 self._highlighter.rehighlightBlock(cursor.block())
182 self._highlighter.rehighlightBlock(cursor.block())
185
183
186 self._previous_prompt_obj = None
184 self._previous_prompt_obj = None
187
185
188 # Show a new prompt with the kernel's estimated prompt number.
186 # Show a new prompt with the kernel's estimated prompt number.
189 next_prompt = content['next_prompt']
187 next_prompt = content['next_prompt']
190 self._show_interpreter_prompt(next_prompt['prompt_number'],
188 self._show_interpreter_prompt(next_prompt['prompt_number'],
191 next_prompt['input_sep'])
189 next_prompt['input_sep'])
192
190
193 #---------------------------------------------------------------------------
191 #---------------------------------------------------------------------------
194 # 'IPythonWidget' interface
192 # 'IPythonWidget' interface
195 #---------------------------------------------------------------------------
193 #---------------------------------------------------------------------------
196
194
197 def edit(self, filename, line=None):
195 def edit(self, filename, line=None):
198 """ Opens a Python script for editing.
196 """ Opens a Python script for editing.
199
197
200 Parameters:
198 Parameters:
201 -----------
199 -----------
202 filename : str
200 filename : str
203 A path to a local system file.
201 A path to a local system file.
204
202
205 line : int, optional
203 line : int, optional
206 A line of interest in the file.
204 A line of interest in the file.
207
205
208 Raises:
206 Raises:
209 -------
207 -------
210 OSError
208 OSError
211 If the editor command cannot be executed.
209 If the editor command cannot be executed.
212 """
210 """
213 if self._editor == 'default':
211 if self._editor == 'default':
214 url = QtCore.QUrl.fromLocalFile(filename)
212 url = QtCore.QUrl.fromLocalFile(filename)
215 if not QtGui.QDesktopServices.openUrl(url):
213 if not QtGui.QDesktopServices.openUrl(url):
216 message = 'Failed to open %s with the default application'
214 message = 'Failed to open %s with the default application'
217 raise OSError(message % repr(filename))
215 raise OSError(message % repr(filename))
218 elif self._editor is None:
216 elif self._editor is None:
219 self.custom_edit_requested.emit(filename, line)
217 self.custom_edit_requested.emit(filename, line)
220 else:
218 else:
221 Popen(self._editor + [filename])
219 Popen(self._editor + [filename])
222
220
223 def reset_styling(self):
221 def reset_styling(self):
224 """ Restores the default IPythonWidget styling.
222 """ Restores the default IPythonWidget styling.
225 """
223 """
226 self.set_styling(self.default_stylesheet, syntax_style='default')
224 self.set_styling(self.default_stylesheet, syntax_style='default')
227 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
225 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
228
226
229 def set_editor(self, editor):
227 def set_editor(self, editor):
230 """ Sets the editor to use with the %edit magic.
228 """ Sets the editor to use with the %edit magic.
231
229
232 Parameters:
230 Parameters:
233 -----------
231 -----------
234 editor : str or sequence of str
232 editor : str or sequence of str
235 A command suitable for use with Popen. This command will be executed
233 A command suitable for use with Popen. This command will be executed
236 with a single argument--a filename--when editing is requested.
234 with a single argument--a filename--when editing is requested.
237
235
238 This parameter also takes two special values:
236 This parameter also takes two special values:
239 'default' : Files will be edited with the system default
237 'default' : Files will be edited with the system default
240 application for Python files.
238 application for Python files.
241 'custom' : Emit a 'custom_edit_requested(str, int)' signal
239 'custom' : Emit a 'custom_edit_requested(str, int)' signal
242 instead of opening an editor.
240 instead of opening an editor.
243 """
241 """
244 if editor == 'default':
242 if editor == 'default':
245 self._editor = 'default'
243 self._editor = 'default'
246 elif editor == 'custom':
244 elif editor == 'custom':
247 self._editor = None
245 self._editor = None
248 elif isinstance(editor, basestring):
246 elif isinstance(editor, basestring):
249 self._editor = [ editor ]
247 self._editor = [ editor ]
250 else:
248 else:
251 self._editor = list(editor)
249 self._editor = list(editor)
252
250
253 def set_styling(self, stylesheet, syntax_style=None):
251 def set_styling(self, stylesheet, syntax_style=None):
254 """ Sets the IPythonWidget styling.
252 """ Sets the IPythonWidget styling.
255
253
256 Parameters:
254 Parameters:
257 -----------
255 -----------
258 stylesheet : str
256 stylesheet : str
259 A CSS stylesheet. The stylesheet can contain classes for:
257 A CSS stylesheet. The stylesheet can contain classes for:
260 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
258 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
261 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
259 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
262 3. IPython: .error, .in-prompt, .out-prompt, etc.
260 3. IPython: .error, .in-prompt, .out-prompt, etc.
263
261
264 syntax_style : str or None [default None]
262 syntax_style : str or None [default None]
265 If specified, use the Pygments style with given name. Otherwise,
263 If specified, use the Pygments style with given name. Otherwise,
266 the stylesheet is queried for Pygments style information.
264 the stylesheet is queried for Pygments style information.
267 """
265 """
268 self.setStyleSheet(stylesheet)
266 self.setStyleSheet(stylesheet)
269 self._control.document().setDefaultStyleSheet(stylesheet)
267 self._control.document().setDefaultStyleSheet(stylesheet)
270 if self._page_control:
268 if self._page_control:
271 self._page_control.document().setDefaultStyleSheet(stylesheet)
269 self._page_control.document().setDefaultStyleSheet(stylesheet)
272
270
273 if syntax_style is None:
271 if syntax_style is None:
274 self._highlighter.set_style_sheet(stylesheet)
272 self._highlighter.set_style_sheet(stylesheet)
275 else:
273 else:
276 self._highlighter.set_style(syntax_style)
274 self._highlighter.set_style(syntax_style)
277
275
278 #---------------------------------------------------------------------------
276 #---------------------------------------------------------------------------
279 # 'IPythonWidget' protected interface
277 # 'IPythonWidget' protected interface
280 #---------------------------------------------------------------------------
278 #---------------------------------------------------------------------------
281
279
282 def _make_in_prompt(self, number):
280 def _make_in_prompt(self, number):
283 """ Given a prompt number, returns an HTML In prompt.
281 """ Given a prompt number, returns an HTML In prompt.
284 """
282 """
285 body = self.in_prompt % number
283 body = self.in_prompt % number
286 return '<span class="in-prompt">%s</span>' % body
284 return '<span class="in-prompt">%s</span>' % body
287
285
288 def _make_continuation_prompt(self, prompt):
286 def _make_continuation_prompt(self, prompt):
289 """ Given a plain text version of an In prompt, returns an HTML
287 """ Given a plain text version of an In prompt, returns an HTML
290 continuation prompt.
288 continuation prompt.
291 """
289 """
292 end_chars = '...: '
290 end_chars = '...: '
293 space_count = len(prompt.lstrip('\n')) - len(end_chars)
291 space_count = len(prompt.lstrip('\n')) - len(end_chars)
294 body = '&nbsp;' * space_count + end_chars
292 body = '&nbsp;' * space_count + end_chars
295 return '<span class="in-prompt">%s</span>' % body
293 return '<span class="in-prompt">%s</span>' % body
296
294
297 def _make_out_prompt(self, number):
295 def _make_out_prompt(self, number):
298 """ Given a prompt number, returns an HTML Out prompt.
296 """ Given a prompt number, returns an HTML Out prompt.
299 """
297 """
300 body = self.out_prompt % number
298 body = self.out_prompt % number
301 return '<span class="out-prompt">%s</span>' % body
299 return '<span class="out-prompt">%s</span>' % body
@@ -1,84 +1,86 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2
2
3 """ A minimal application using the Qt console-style IPython frontend.
3 """ A minimal application using the Qt console-style IPython frontend.
4 """
4 """
5
5
6 # Systemm library imports
6 # Systemm library imports
7 from PyQt4 import QtCore, QtGui
7 from PyQt4 import QtCore, QtGui
8
8
9 # Local imports
9 # Local imports
10 from IPython.external.argparse import ArgumentParser
10 from IPython.external.argparse import ArgumentParser
11 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
11 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
12 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
12 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
13 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
13 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
14 from IPython.frontend.qt.kernelmanager import QtKernelManager
14 from IPython.frontend.qt.kernelmanager import QtKernelManager
15
15
16 # Constants
16 # Constants
17 LOCALHOST = '127.0.0.1'
17 LOCALHOST = '127.0.0.1'
18
18
19
19
20 def main():
20 def main():
21 """ Entry point for application.
21 """ Entry point for application.
22 """
22 """
23 # Parse command line arguments.
23 # Parse command line arguments.
24 parser = ArgumentParser()
24 parser = ArgumentParser()
25 parser.add_argument('-e', '--existing', action='store_true',
25 parser.add_argument('-e', '--existing', action='store_true',
26 help='connect to an existing kernel')
26 help='connect to an existing kernel')
27 parser.add_argument('--ip', type=str, default=LOCALHOST,
27 parser.add_argument('--ip', type=str, default=LOCALHOST,
28 help='set the kernel\'s IP address [default localhost]')
28 help='set the kernel\'s IP address [default localhost]')
29 parser.add_argument('--xreq', type=int, metavar='PORT', default=0,
29 parser.add_argument('--xreq', type=int, metavar='PORT', default=0,
30 help='set the XREQ channel port [default random]')
30 help='set the XREQ channel port [default random]')
31 parser.add_argument('--sub', type=int, metavar='PORT', default=0,
31 parser.add_argument('--sub', type=int, metavar='PORT', default=0,
32 help='set the SUB channel port [default random]')
32 help='set the SUB channel port [default random]')
33 parser.add_argument('--rep', type=int, metavar='PORT', default=0,
33 parser.add_argument('--rep', type=int, metavar='PORT', default=0,
34 help='set the REP channel port [default random]')
34 help='set the REP channel port [default random]')
35 group = parser.add_mutually_exclusive_group()
35 group = parser.add_mutually_exclusive_group()
36 group.add_argument('--pure', action='store_true', help = \
36 group.add_argument('--pure', action='store_true', help = \
37 'use a pure Python kernel instead of an IPython kernel')
37 'use a pure Python kernel instead of an IPython kernel')
38 group.add_argument('--pylab', action='store_true',
38 group.add_argument('--pylab', action='store_true',
39 help='use a kernel with PyLab enabled')
39 help='use a kernel with PyLab enabled')
40 parser.add_argument('--rich', action='store_true',
40 parser.add_argument('--rich', action='store_true',
41 help='use a rich text frontend')
41 help='use a rich text frontend')
42 args = parser.parse_args()
42 args = parser.parse_args()
43
43
44 # Don't let Qt or ZMQ swallow KeyboardInterupts.
44 # Don't let Qt or ZMQ swallow KeyboardInterupts.
45 import signal
45 import signal
46 signal.signal(signal.SIGINT, signal.SIG_DFL)
46 signal.signal(signal.SIGINT, signal.SIG_DFL)
47
47
48 # Create a KernelManager and start a kernel.
48 # Create a KernelManager and start a kernel.
49 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
49 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
50 sub_address=(args.ip, args.sub),
50 sub_address=(args.ip, args.sub),
51 rep_address=(args.ip, args.rep))
51 rep_address=(args.ip, args.rep))
52 if args.ip == LOCALHOST and not args.existing:
52 if args.ip == LOCALHOST and not args.existing:
53 if args.pure:
53 if args.pure:
54 kernel_manager.start_kernel(ipython=False)
54 kernel_manager.start_kernel(ipython=False)
55 elif args.pylab:
55 elif args.pylab:
56 if args.rich:
56 if args.rich:
57 kernel_manager.start_kernel(pylab='payload-svg')
57 kernel_manager.start_kernel(pylab='payload-svg')
58 else:
58 else:
59 kernel_manager.start_kernel(pylab='qt4')
59 kernel_manager.start_kernel(pylab='qt4')
60 else:
60 else:
61 kernel_manager.start_kernel()
61 kernel_manager.start_kernel()
62 kernel_manager.start_channels()
62 kernel_manager.start_channels()
63
63
64 # FIXME: this is a hack, set colors to lightbg by default in qt terminal
64 # Create the widget.
65 # unconditionally, regardless of user settings in config files.
66 kernel_manager.xreq_channel.execute("%colors lightbg")
67
68 # Launch the application.
69 app = QtGui.QApplication([])
65 app = QtGui.QApplication([])
70 if args.pure:
66 if args.pure:
71 kind = 'rich' if args.rich else 'plain'
67 kind = 'rich' if args.rich else 'plain'
72 widget = FrontendWidget(kind=kind)
68 widget = FrontendWidget(kind=kind)
73 elif args.rich:
69 elif args.rich:
74 widget = RichIPythonWidget()
70 widget = RichIPythonWidget()
75 else:
71 else:
76 widget = IPythonWidget()
72 widget = IPythonWidget()
77 widget.kernel_manager = kernel_manager
73 widget.kernel_manager = kernel_manager
78 widget.setWindowTitle('Python' if args.pure else 'IPython')
74 widget.setWindowTitle('Python' if args.pure else 'IPython')
79 widget.show()
75 widget.show()
76
77 # FIXME: This is a hack: set colors to lightbg by default in qt terminal
78 # unconditionally, regardless of user settings in config files.
79 widget.execute("%colors lightbg", hidden=True)
80
81 # Start the application main loop.
80 app.exec_()
82 app.exec_()
81
83
82
84
83 if __name__ == '__main__':
85 if __name__ == '__main__':
84 main()
86 main()
@@ -1,578 +1,581 b''
1 """Base classes to manage the interaction with a running kernel.
1 """Base 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, TCPAddress
32 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
33 from session import Session
33 from session import Session
34
34
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36 # Constants and exceptions
36 # Constants and exceptions
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38
38
39 LOCALHOST = '127.0.0.1'
39 LOCALHOST = '127.0.0.1'
40
40
41 class InvalidPortNumber(Exception):
41 class InvalidPortNumber(Exception):
42 pass
42 pass
43
43
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45 # ZMQ Socket Channel classes
45 # ZMQ Socket Channel classes
46 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
47
47
48 class ZmqSocketChannel(Thread):
48 class ZmqSocketChannel(Thread):
49 """The base class for the channels that use ZMQ sockets.
49 """The base class for the channels that use ZMQ sockets.
50 """
50 """
51 context = None
51 context = None
52 session = None
52 session = None
53 socket = None
53 socket = None
54 ioloop = None
54 ioloop = None
55 iostate = None
55 iostate = None
56 _address = None
56 _address = None
57
57
58 def __init__(self, context, session, address):
58 def __init__(self, context, session, address):
59 """Create a channel
59 """Create a channel
60
60
61 Parameters
61 Parameters
62 ----------
62 ----------
63 context : :class:`zmq.Context`
63 context : :class:`zmq.Context`
64 The ZMQ context to use.
64 The ZMQ context to use.
65 session : :class:`session.Session`
65 session : :class:`session.Session`
66 The session to use.
66 The session to use.
67 address : tuple
67 address : tuple
68 Standard (ip, port) tuple that the kernel is listening on.
68 Standard (ip, port) tuple that the kernel is listening on.
69 """
69 """
70 super(ZmqSocketChannel, self).__init__()
70 super(ZmqSocketChannel, self).__init__()
71 self.daemon = True
71 self.daemon = True
72
72
73 self.context = context
73 self.context = context
74 self.session = session
74 self.session = session
75 if address[1] == 0:
75 if address[1] == 0:
76 message = 'The port number for a channel cannot be 0.'
76 message = 'The port number for a channel cannot be 0.'
77 raise InvalidPortNumber(message)
77 raise InvalidPortNumber(message)
78 self._address = address
78 self._address = address
79
79
80 def stop(self):
80 def stop(self):
81 """Stop the channel's activity.
81 """Stop the channel's activity.
82
82
83 This calls :method:`Thread.join` and returns when the thread
83 This calls :method:`Thread.join` and returns when the thread
84 terminates. :class:`RuntimeError` will be raised if
84 terminates. :class:`RuntimeError` will be raised if
85 :method:`self.start` is called again.
85 :method:`self.start` is called again.
86 """
86 """
87 self.join()
87 self.join()
88
88
89 @property
89 @property
90 def address(self):
90 def address(self):
91 """Get the channel's address as an (ip, port) tuple.
91 """Get the channel's address as an (ip, port) tuple.
92
92
93 By the default, the address is (localhost, 0), where 0 means a random
93 By the default, the address is (localhost, 0), where 0 means a random
94 port.
94 port.
95 """
95 """
96 return self._address
96 return self._address
97
97
98 def add_io_state(self, state):
98 def add_io_state(self, state):
99 """Add IO state to the eventloop.
99 """Add IO state to the eventloop.
100
100
101 Parameters
101 Parameters
102 ----------
102 ----------
103 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
103 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
104 The IO state flag to set.
104 The IO state flag to set.
105
105
106 This is thread safe as it uses the thread safe IOLoop.add_callback.
106 This is thread safe as it uses the thread safe IOLoop.add_callback.
107 """
107 """
108 def add_io_state_callback():
108 def add_io_state_callback():
109 if not self.iostate & state:
109 if not self.iostate & state:
110 self.iostate = self.iostate | state
110 self.iostate = self.iostate | state
111 self.ioloop.update_handler(self.socket, self.iostate)
111 self.ioloop.update_handler(self.socket, self.iostate)
112 self.ioloop.add_callback(add_io_state_callback)
112 self.ioloop.add_callback(add_io_state_callback)
113
113
114 def drop_io_state(self, state):
114 def drop_io_state(self, state):
115 """Drop IO state from the eventloop.
115 """Drop IO state from the eventloop.
116
116
117 Parameters
117 Parameters
118 ----------
118 ----------
119 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
119 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
120 The IO state flag to set.
120 The IO state flag to set.
121
121
122 This is thread safe as it uses the thread safe IOLoop.add_callback.
122 This is thread safe as it uses the thread safe IOLoop.add_callback.
123 """
123 """
124 def drop_io_state_callback():
124 def drop_io_state_callback():
125 if self.iostate & state:
125 if self.iostate & state:
126 self.iostate = self.iostate & (~state)
126 self.iostate = self.iostate & (~state)
127 self.ioloop.update_handler(self.socket, self.iostate)
127 self.ioloop.update_handler(self.socket, self.iostate)
128 self.ioloop.add_callback(drop_io_state_callback)
128 self.ioloop.add_callback(drop_io_state_callback)
129
129
130
130
131 class XReqSocketChannel(ZmqSocketChannel):
131 class XReqSocketChannel(ZmqSocketChannel):
132 """The XREQ channel for issues request/replies to the kernel.
132 """The XREQ channel for issues request/replies to the kernel.
133 """
133 """
134
134
135 command_queue = None
135 command_queue = None
136
136
137 def __init__(self, context, session, address):
137 def __init__(self, context, session, address):
138 self.command_queue = Queue()
138 self.command_queue = Queue()
139 super(XReqSocketChannel, self).__init__(context, session, address)
139 super(XReqSocketChannel, self).__init__(context, session, address)
140
140
141 def run(self):
141 def run(self):
142 """The thread's main activity. Call start() instead."""
142 """The thread's main activity. Call start() instead."""
143 self.socket = self.context.socket(zmq.XREQ)
143 self.socket = self.context.socket(zmq.XREQ)
144 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
144 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
145 self.socket.connect('tcp://%s:%i' % self.address)
145 self.socket.connect('tcp://%s:%i' % self.address)
146 self.ioloop = ioloop.IOLoop()
146 self.ioloop = ioloop.IOLoop()
147 self.iostate = POLLERR|POLLIN
147 self.iostate = POLLERR|POLLIN
148 self.ioloop.add_handler(self.socket, self._handle_events,
148 self.ioloop.add_handler(self.socket, self._handle_events,
149 self.iostate)
149 self.iostate)
150 self.ioloop.start()
150 self.ioloop.start()
151
151
152 def stop(self):
152 def stop(self):
153 self.ioloop.stop()
153 self.ioloop.stop()
154 super(XReqSocketChannel, self).stop()
154 super(XReqSocketChannel, self).stop()
155
155
156 def call_handlers(self, msg):
156 def call_handlers(self, msg):
157 """This method is called in the ioloop thread when a message arrives.
157 """This method is called in the ioloop thread when a message arrives.
158
158
159 Subclasses should override this method to handle incoming messages.
159 Subclasses should override this method to handle incoming messages.
160 It is important to remember that this method is called in the thread
160 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
161 so that some logic must be done to ensure that the application leve
162 handlers are called in the application thread.
162 handlers are called in the application thread.
163 """
163 """
164 raise NotImplementedError('call_handlers must be defined in a subclass.')
164 raise NotImplementedError('call_handlers must be defined in a subclass.')
165
165
166 def execute(self, code):
166 def execute(self, code):
167 """Execute code in the kernel.
167 """Execute code in the kernel.
168
168
169 Parameters
169 Parameters
170 ----------
170 ----------
171 code : str
171 code : str
172 A string of Python code.
172 A string of Python code.
173
173
174 Returns
174 Returns
175 -------
175 -------
176 The msg_id of the message sent.
176 The msg_id of the message sent.
177 """
177 """
178 # Create class for content/msg creation. Related to, but possibly
178 # Create class for content/msg creation. Related to, but possibly
179 # not in Session.
179 # not in Session.
180 content = dict(code=code)
180 content = dict(code=code)
181 msg = self.session.msg('execute_request', content)
181 msg = self.session.msg('execute_request', content)
182 self._queue_request(msg)
182 self._queue_request(msg)
183 return msg['header']['msg_id']
183 return msg['header']['msg_id']
184
184
185 def complete(self, text, line, cursor_pos, block=None):
185 def complete(self, text, line, cursor_pos, block=None):
186 """Tab complete text, line, block in the kernel's namespace.
186 """Tab complete text in the kernel's namespace.
187
187
188 Parameters
188 Parameters
189 ----------
189 ----------
190 text : str
190 text : str
191 The text to complete.
191 The text to complete.
192 line : str
192 line : str
193 The full line of text that is the surrounding context for the
193 The full line of text that is the surrounding context for the
194 text to complete.
194 text to complete.
195 block : str
195 cursor_pos : int
196 The position of the cursor in the line where the completion was
197 requested.
198 block : str, optional
196 The full block of code in which the completion is being requested.
199 The full block of code in which the completion is being requested.
197
200
198 Returns
201 Returns
199 -------
202 -------
200 The msg_id of the message sent.
203 The msg_id of the message sent.
201 """
204 """
202 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
205 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
203 msg = self.session.msg('complete_request', content)
206 msg = self.session.msg('complete_request', content)
204 self._queue_request(msg)
207 self._queue_request(msg)
205 return msg['header']['msg_id']
208 return msg['header']['msg_id']
206
209
207 def object_info(self, oname):
210 def object_info(self, oname):
208 """Get metadata information about an object.
211 """Get metadata information about an object.
209
212
210 Parameters
213 Parameters
211 ----------
214 ----------
212 oname : str
215 oname : str
213 A string specifying the object name.
216 A string specifying the object name.
214
217
215 Returns
218 Returns
216 -------
219 -------
217 The msg_id of the message sent.
220 The msg_id of the message sent.
218 """
221 """
219 content = dict(oname=oname)
222 content = dict(oname=oname)
220 msg = self.session.msg('object_info_request', content)
223 msg = self.session.msg('object_info_request', content)
221 self._queue_request(msg)
224 self._queue_request(msg)
222 return msg['header']['msg_id']
225 return msg['header']['msg_id']
223
226
224 def _handle_events(self, socket, events):
227 def _handle_events(self, socket, events):
225 if events & POLLERR:
228 if events & POLLERR:
226 self._handle_err()
229 self._handle_err()
227 if events & POLLOUT:
230 if events & POLLOUT:
228 self._handle_send()
231 self._handle_send()
229 if events & POLLIN:
232 if events & POLLIN:
230 self._handle_recv()
233 self._handle_recv()
231
234
232 def _handle_recv(self):
235 def _handle_recv(self):
233 msg = self.socket.recv_json()
236 msg = self.socket.recv_json()
234 self.call_handlers(msg)
237 self.call_handlers(msg)
235
238
236 def _handle_send(self):
239 def _handle_send(self):
237 try:
240 try:
238 msg = self.command_queue.get(False)
241 msg = self.command_queue.get(False)
239 except Empty:
242 except Empty:
240 pass
243 pass
241 else:
244 else:
242 self.socket.send_json(msg)
245 self.socket.send_json(msg)
243 if self.command_queue.empty():
246 if self.command_queue.empty():
244 self.drop_io_state(POLLOUT)
247 self.drop_io_state(POLLOUT)
245
248
246 def _handle_err(self):
249 def _handle_err(self):
247 # We don't want to let this go silently, so eventually we should log.
250 # We don't want to let this go silently, so eventually we should log.
248 raise zmq.ZMQError()
251 raise zmq.ZMQError()
249
252
250 def _queue_request(self, msg):
253 def _queue_request(self, msg):
251 self.command_queue.put(msg)
254 self.command_queue.put(msg)
252 self.add_io_state(POLLOUT)
255 self.add_io_state(POLLOUT)
253
256
254
257
255 class SubSocketChannel(ZmqSocketChannel):
258 class SubSocketChannel(ZmqSocketChannel):
256 """The SUB channel which listens for messages that the kernel publishes.
259 """The SUB channel which listens for messages that the kernel publishes.
257 """
260 """
258
261
259 def __init__(self, context, session, address):
262 def __init__(self, context, session, address):
260 super(SubSocketChannel, self).__init__(context, session, address)
263 super(SubSocketChannel, self).__init__(context, session, address)
261
264
262 def run(self):
265 def run(self):
263 """The thread's main activity. Call start() instead."""
266 """The thread's main activity. Call start() instead."""
264 self.socket = self.context.socket(zmq.SUB)
267 self.socket = self.context.socket(zmq.SUB)
265 self.socket.setsockopt(zmq.SUBSCRIBE,'')
268 self.socket.setsockopt(zmq.SUBSCRIBE,'')
266 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
269 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
267 self.socket.connect('tcp://%s:%i' % self.address)
270 self.socket.connect('tcp://%s:%i' % self.address)
268 self.ioloop = ioloop.IOLoop()
271 self.ioloop = ioloop.IOLoop()
269 self.iostate = POLLIN|POLLERR
272 self.iostate = POLLIN|POLLERR
270 self.ioloop.add_handler(self.socket, self._handle_events,
273 self.ioloop.add_handler(self.socket, self._handle_events,
271 self.iostate)
274 self.iostate)
272 self.ioloop.start()
275 self.ioloop.start()
273
276
274 def stop(self):
277 def stop(self):
275 self.ioloop.stop()
278 self.ioloop.stop()
276 super(SubSocketChannel, self).stop()
279 super(SubSocketChannel, self).stop()
277
280
278 def call_handlers(self, msg):
281 def call_handlers(self, msg):
279 """This method is called in the ioloop thread when a message arrives.
282 """This method is called in the ioloop thread when a message arrives.
280
283
281 Subclasses should override this method to handle incoming messages.
284 Subclasses should override this method to handle incoming messages.
282 It is important to remember that this method is called in the thread
285 It is important to remember that this method is called in the thread
283 so that some logic must be done to ensure that the application leve
286 so that some logic must be done to ensure that the application leve
284 handlers are called in the application thread.
287 handlers are called in the application thread.
285 """
288 """
286 raise NotImplementedError('call_handlers must be defined in a subclass.')
289 raise NotImplementedError('call_handlers must be defined in a subclass.')
287
290
288 def flush(self, timeout=1.0):
291 def flush(self, timeout=1.0):
289 """Immediately processes all pending messages on the SUB channel.
292 """Immediately processes all pending messages on the SUB channel.
290
293
291 Callers should use this method to ensure that :method:`call_handlers`
294 Callers should use this method to ensure that :method:`call_handlers`
292 has been called for all messages that have been received on the
295 has been called for all messages that have been received on the
293 0MQ SUB socket of this channel.
296 0MQ SUB socket of this channel.
294
297
295 This method is thread safe.
298 This method is thread safe.
296
299
297 Parameters
300 Parameters
298 ----------
301 ----------
299 timeout : float, optional
302 timeout : float, optional
300 The maximum amount of time to spend flushing, in seconds. The
303 The maximum amount of time to spend flushing, in seconds. The
301 default is one second.
304 default is one second.
302 """
305 """
303 # We do the IOLoop callback process twice to ensure that the IOLoop
306 # We do the IOLoop callback process twice to ensure that the IOLoop
304 # gets to perform at least one full poll.
307 # gets to perform at least one full poll.
305 stop_time = time.time() + timeout
308 stop_time = time.time() + timeout
306 for i in xrange(2):
309 for i in xrange(2):
307 self._flushed = False
310 self._flushed = False
308 self.ioloop.add_callback(self._flush)
311 self.ioloop.add_callback(self._flush)
309 while not self._flushed and time.time() < stop_time:
312 while not self._flushed and time.time() < stop_time:
310 time.sleep(0.01)
313 time.sleep(0.01)
311
314
312 def _handle_events(self, socket, events):
315 def _handle_events(self, socket, events):
313 # Turn on and off POLLOUT depending on if we have made a request
316 # Turn on and off POLLOUT depending on if we have made a request
314 if events & POLLERR:
317 if events & POLLERR:
315 self._handle_err()
318 self._handle_err()
316 if events & POLLIN:
319 if events & POLLIN:
317 self._handle_recv()
320 self._handle_recv()
318
321
319 def _handle_err(self):
322 def _handle_err(self):
320 # We don't want to let this go silently, so eventually we should log.
323 # We don't want to let this go silently, so eventually we should log.
321 raise zmq.ZMQError()
324 raise zmq.ZMQError()
322
325
323 def _handle_recv(self):
326 def _handle_recv(self):
324 # Get all of the messages we can
327 # Get all of the messages we can
325 while True:
328 while True:
326 try:
329 try:
327 msg = self.socket.recv_json(zmq.NOBLOCK)
330 msg = self.socket.recv_json(zmq.NOBLOCK)
328 except zmq.ZMQError:
331 except zmq.ZMQError:
329 # Check the errno?
332 # Check the errno?
330 # Will this trigger POLLERR?
333 # Will this trigger POLLERR?
331 break
334 break
332 else:
335 else:
333 self.call_handlers(msg)
336 self.call_handlers(msg)
334
337
335 def _flush(self):
338 def _flush(self):
336 """Callback for :method:`self.flush`."""
339 """Callback for :method:`self.flush`."""
337 self._flushed = True
340 self._flushed = True
338
341
339
342
340 class RepSocketChannel(ZmqSocketChannel):
343 class RepSocketChannel(ZmqSocketChannel):
341 """A reply channel to handle raw_input requests that the kernel makes."""
344 """A reply channel to handle raw_input requests that the kernel makes."""
342
345
343 msg_queue = None
346 msg_queue = None
344
347
345 def __init__(self, context, session, address):
348 def __init__(self, context, session, address):
346 self.msg_queue = Queue()
349 self.msg_queue = Queue()
347 super(RepSocketChannel, self).__init__(context, session, address)
350 super(RepSocketChannel, self).__init__(context, session, address)
348
351
349 def run(self):
352 def run(self):
350 """The thread's main activity. Call start() instead."""
353 """The thread's main activity. Call start() instead."""
351 self.socket = self.context.socket(zmq.XREQ)
354 self.socket = self.context.socket(zmq.XREQ)
352 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
355 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
353 self.socket.connect('tcp://%s:%i' % self.address)
356 self.socket.connect('tcp://%s:%i' % self.address)
354 self.ioloop = ioloop.IOLoop()
357 self.ioloop = ioloop.IOLoop()
355 self.iostate = POLLERR|POLLIN
358 self.iostate = POLLERR|POLLIN
356 self.ioloop.add_handler(self.socket, self._handle_events,
359 self.ioloop.add_handler(self.socket, self._handle_events,
357 self.iostate)
360 self.iostate)
358 self.ioloop.start()
361 self.ioloop.start()
359
362
360 def stop(self):
363 def stop(self):
361 self.ioloop.stop()
364 self.ioloop.stop()
362 super(RepSocketChannel, self).stop()
365 super(RepSocketChannel, self).stop()
363
366
364 def call_handlers(self, msg):
367 def call_handlers(self, msg):
365 """This method is called in the ioloop thread when a message arrives.
368 """This method is called in the ioloop thread when a message arrives.
366
369
367 Subclasses should override this method to handle incoming messages.
370 Subclasses should override this method to handle incoming messages.
368 It is important to remember that this method is called in the thread
371 It is important to remember that this method is called in the thread
369 so that some logic must be done to ensure that the application leve
372 so that some logic must be done to ensure that the application leve
370 handlers are called in the application thread.
373 handlers are called in the application thread.
371 """
374 """
372 raise NotImplementedError('call_handlers must be defined in a subclass.')
375 raise NotImplementedError('call_handlers must be defined in a subclass.')
373
376
374 def input(self, string):
377 def input(self, string):
375 """Send a string of raw input to the kernel."""
378 """Send a string of raw input to the kernel."""
376 content = dict(value=string)
379 content = dict(value=string)
377 msg = self.session.msg('input_reply', content)
380 msg = self.session.msg('input_reply', content)
378 self._queue_reply(msg)
381 self._queue_reply(msg)
379
382
380 def _handle_events(self, socket, events):
383 def _handle_events(self, socket, events):
381 if events & POLLERR:
384 if events & POLLERR:
382 self._handle_err()
385 self._handle_err()
383 if events & POLLOUT:
386 if events & POLLOUT:
384 self._handle_send()
387 self._handle_send()
385 if events & POLLIN:
388 if events & POLLIN:
386 self._handle_recv()
389 self._handle_recv()
387
390
388 def _handle_recv(self):
391 def _handle_recv(self):
389 msg = self.socket.recv_json()
392 msg = self.socket.recv_json()
390 self.call_handlers(msg)
393 self.call_handlers(msg)
391
394
392 def _handle_send(self):
395 def _handle_send(self):
393 try:
396 try:
394 msg = self.msg_queue.get(False)
397 msg = self.msg_queue.get(False)
395 except Empty:
398 except Empty:
396 pass
399 pass
397 else:
400 else:
398 self.socket.send_json(msg)
401 self.socket.send_json(msg)
399 if self.msg_queue.empty():
402 if self.msg_queue.empty():
400 self.drop_io_state(POLLOUT)
403 self.drop_io_state(POLLOUT)
401
404
402 def _handle_err(self):
405 def _handle_err(self):
403 # We don't want to let this go silently, so eventually we should log.
406 # We don't want to let this go silently, so eventually we should log.
404 raise zmq.ZMQError()
407 raise zmq.ZMQError()
405
408
406 def _queue_reply(self, msg):
409 def _queue_reply(self, msg):
407 self.msg_queue.put(msg)
410 self.msg_queue.put(msg)
408 self.add_io_state(POLLOUT)
411 self.add_io_state(POLLOUT)
409
412
410
413
411 #-----------------------------------------------------------------------------
414 #-----------------------------------------------------------------------------
412 # Main kernel manager class
415 # Main kernel manager class
413 #-----------------------------------------------------------------------------
416 #-----------------------------------------------------------------------------
414
417
415 class KernelManager(HasTraits):
418 class KernelManager(HasTraits):
416 """ Manages a kernel for a frontend.
419 """ Manages a kernel for a frontend.
417
420
418 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
419 kernel.
422 kernel.
420
423
421 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.
422
425
423 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
424 frontend.
427 frontend.
425 """
428 """
426 # The PyZMQ Context to use for communication with the kernel.
429 # The PyZMQ Context to use for communication with the kernel.
427 context = Instance(zmq.Context,(),{})
430 context = Instance(zmq.Context,(),{})
428
431
429 # The Session to use for communication with the kernel.
432 # The Session to use for communication with the kernel.
430 session = Instance(Session,(),{})
433 session = Instance(Session,(),{})
431
434
432 # The kernel process with which the KernelManager is communicating.
435 # The kernel process with which the KernelManager is communicating.
433 kernel = Instance(Popen)
436 kernel = Instance(Popen)
434
437
435 # The addresses for the communication channels.
438 # The addresses for the communication channels.
436 xreq_address = TCPAddress((LOCALHOST, 0))
439 xreq_address = TCPAddress((LOCALHOST, 0))
437 sub_address = TCPAddress((LOCALHOST, 0))
440 sub_address = TCPAddress((LOCALHOST, 0))
438 rep_address = TCPAddress((LOCALHOST, 0))
441 rep_address = TCPAddress((LOCALHOST, 0))
439
442
440 # The classes to use for the various channels.
443 # The classes to use for the various channels.
441 xreq_channel_class = Type(XReqSocketChannel)
444 xreq_channel_class = Type(XReqSocketChannel)
442 sub_channel_class = Type(SubSocketChannel)
445 sub_channel_class = Type(SubSocketChannel)
443 rep_channel_class = Type(RepSocketChannel)
446 rep_channel_class = Type(RepSocketChannel)
444
447
445 # Protected traits.
448 # Protected traits.
446 _xreq_channel = Any
449 _xreq_channel = Any
447 _sub_channel = Any
450 _sub_channel = Any
448 _rep_channel = Any
451 _rep_channel = Any
449
452
450 #--------------------------------------------------------------------------
453 #--------------------------------------------------------------------------
451 # Channel management methods:
454 # Channel management methods:
452 #--------------------------------------------------------------------------
455 #--------------------------------------------------------------------------
453
456
454 def start_channels(self):
457 def start_channels(self):
455 """Starts the channels for this kernel.
458 """Starts the channels for this kernel.
456
459
457 This will create the channels if they do not exist and then start
460 This will create the channels if they do not exist and then start
458 them. If port numbers of 0 are being used (random ports) then you
461 them. If port numbers of 0 are being used (random ports) then you
459 must first call :method:`start_kernel`. If the channels have been
462 must first call :method:`start_kernel`. If the channels have been
460 stopped and you call this, :class:`RuntimeError` will be raised.
463 stopped and you call this, :class:`RuntimeError` will be raised.
461 """
464 """
462 self.xreq_channel.start()
465 self.xreq_channel.start()
463 self.sub_channel.start()
466 self.sub_channel.start()
464 self.rep_channel.start()
467 self.rep_channel.start()
465
468
466 def stop_channels(self):
469 def stop_channels(self):
467 """Stops the channels for this kernel.
470 """Stops the channels for this kernel.
468
471
469 This stops the channels by joining their threads. If the channels
472 This stops the channels by joining their threads. If the channels
470 were not started, :class:`RuntimeError` will be raised.
473 were not started, :class:`RuntimeError` will be raised.
471 """
474 """
472 self.xreq_channel.stop()
475 self.xreq_channel.stop()
473 self.sub_channel.stop()
476 self.sub_channel.stop()
474 self.rep_channel.stop()
477 self.rep_channel.stop()
475
478
476 @property
479 @property
477 def channels_running(self):
480 def channels_running(self):
478 """Are all of the channels created and running?"""
481 """Are all of the channels created and running?"""
479 return self.xreq_channel.is_alive() \
482 return self.xreq_channel.is_alive() \
480 and self.sub_channel.is_alive() \
483 and self.sub_channel.is_alive() \
481 and self.rep_channel.is_alive()
484 and self.rep_channel.is_alive()
482
485
483 #--------------------------------------------------------------------------
486 #--------------------------------------------------------------------------
484 # Kernel process management methods:
487 # Kernel process management methods:
485 #--------------------------------------------------------------------------
488 #--------------------------------------------------------------------------
486
489
487 def start_kernel(self, ipython=True, **kw):
490 def start_kernel(self, ipython=True, **kw):
488 """Starts a kernel process and configures the manager to use it.
491 """Starts a kernel process and configures the manager to use it.
489
492
490 If random ports (port=0) are being used, this method must be called
493 If random ports (port=0) are being used, this method must be called
491 before the channels are created.
494 before the channels are created.
492
495
493 Parameters:
496 Parameters:
494 -----------
497 -----------
495 ipython : bool, optional (default True)
498 ipython : bool, optional (default True)
496 Whether to use an IPython kernel instead of a plain Python kernel.
499 Whether to use an IPython kernel instead of a plain Python kernel.
497 """
500 """
498 xreq, sub, rep = self.xreq_address, self.sub_address, self.rep_address
501 xreq, sub, rep = self.xreq_address, self.sub_address, self.rep_address
499 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or rep[0] != LOCALHOST:
502 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or rep[0] != LOCALHOST:
500 raise RuntimeError("Can only launch a kernel on localhost."
503 raise RuntimeError("Can only launch a kernel on localhost."
501 "Make sure that the '*_address' attributes are "
504 "Make sure that the '*_address' attributes are "
502 "configured properly.")
505 "configured properly.")
503
506
504 if ipython:
507 if ipython:
505 from ipkernel import launch_kernel as launch
508 from ipkernel import launch_kernel as launch
506 else:
509 else:
507 from pykernel import launch_kernel as launch
510 from pykernel import launch_kernel as launch
508 self.kernel, xrep, pub, req = launch(xrep_port=xreq[1], pub_port=sub[1],
511 self.kernel, xrep, pub, req = launch(xrep_port=xreq[1], pub_port=sub[1],
509 req_port=rep[1], **kw)
512 req_port=rep[1], **kw)
510 self.xreq_address = (LOCALHOST, xrep)
513 self.xreq_address = (LOCALHOST, xrep)
511 self.sub_address = (LOCALHOST, pub)
514 self.sub_address = (LOCALHOST, pub)
512 self.rep_address = (LOCALHOST, req)
515 self.rep_address = (LOCALHOST, req)
513
516
514 @property
517 @property
515 def has_kernel(self):
518 def has_kernel(self):
516 """Returns whether a kernel process has been specified for the kernel
519 """Returns whether a kernel process has been specified for the kernel
517 manager.
520 manager.
518 """
521 """
519 return self.kernel is not None
522 return self.kernel is not None
520
523
521 def kill_kernel(self):
524 def kill_kernel(self):
522 """ Kill the running kernel. """
525 """ Kill the running kernel. """
523 if self.kernel is not None:
526 if self.kernel is not None:
524 self.kernel.kill()
527 self.kernel.kill()
525 self.kernel = None
528 self.kernel = None
526 else:
529 else:
527 raise RuntimeError("Cannot kill kernel. No kernel is running!")
530 raise RuntimeError("Cannot kill kernel. No kernel is running!")
528
531
529 def signal_kernel(self, signum):
532 def signal_kernel(self, signum):
530 """ Sends a signal to the kernel. """
533 """ Sends a signal to the kernel. """
531 if self.kernel is not None:
534 if self.kernel is not None:
532 self.kernel.send_signal(signum)
535 self.kernel.send_signal(signum)
533 else:
536 else:
534 raise RuntimeError("Cannot signal kernel. No kernel is running!")
537 raise RuntimeError("Cannot signal kernel. No kernel is running!")
535
538
536 @property
539 @property
537 def is_alive(self):
540 def is_alive(self):
538 """Is the kernel process still running?"""
541 """Is the kernel process still running?"""
539 if self.kernel is not None:
542 if self.kernel is not None:
540 if self.kernel.poll() is None:
543 if self.kernel.poll() is None:
541 return True
544 return True
542 else:
545 else:
543 return False
546 return False
544 else:
547 else:
545 # 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
546 # 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.
547 return True
550 return True
548
551
549 #--------------------------------------------------------------------------
552 #--------------------------------------------------------------------------
550 # Channels used for communication with the kernel:
553 # Channels used for communication with the kernel:
551 #--------------------------------------------------------------------------
554 #--------------------------------------------------------------------------
552
555
553 @property
556 @property
554 def xreq_channel(self):
557 def xreq_channel(self):
555 """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."""
556 if self._xreq_channel is None:
559 if self._xreq_channel is None:
557 self._xreq_channel = self.xreq_channel_class(self.context,
560 self._xreq_channel = self.xreq_channel_class(self.context,
558 self.session,
561 self.session,
559 self.xreq_address)
562 self.xreq_address)
560 return self._xreq_channel
563 return self._xreq_channel
561
564
562 @property
565 @property
563 def sub_channel(self):
566 def sub_channel(self):
564 """Get the SUB socket channel object."""
567 """Get the SUB socket channel object."""
565 if self._sub_channel is None:
568 if self._sub_channel is None:
566 self._sub_channel = self.sub_channel_class(self.context,
569 self._sub_channel = self.sub_channel_class(self.context,
567 self.session,
570 self.session,
568 self.sub_address)
571 self.sub_address)
569 return self._sub_channel
572 return self._sub_channel
570
573
571 @property
574 @property
572 def rep_channel(self):
575 def rep_channel(self):
573 """Get the REP socket channel object to handle stdin (raw_input)."""
576 """Get the REP socket channel object to handle stdin (raw_input)."""
574 if self._rep_channel is None:
577 if self._rep_channel is None:
575 self._rep_channel = self.rep_channel_class(self.context,
578 self._rep_channel = self.rep_channel_class(self.context,
576 self.session,
579 self.session,
577 self.rep_address)
580 self.rep_address)
578 return self._rep_channel
581 return self._rep_channel
General Comments 0
You need to be logged in to leave comments. Login now