##// END OF EJS Templates
* Added support for prompt and history requests to the kernel manager and Qt console frontend....
epatters -
Show More
@@ -1,1279 +1,1290 b''
1 # Standard library imports
1 # Standard library imports
2 import re
2 import re
3 import sys
3 import sys
4 from textwrap import dedent
4 from textwrap import dedent
5
5
6 # System library imports
6 # System library imports
7 from PyQt4 import QtCore, QtGui
7 from PyQt4 import QtCore, QtGui
8
8
9 # Local imports
9 # Local imports
10 from ansi_code_processor import QtAnsiCodeProcessor
10 from ansi_code_processor import QtAnsiCodeProcessor
11 from completion_widget import CompletionWidget
11 from completion_widget import CompletionWidget
12
12
13
13
14 class ConsoleWidget(QtGui.QWidget):
14 class ConsoleWidget(QtGui.QWidget):
15 """ An abstract base class for console-type widgets. This class has
15 """ An abstract base class for console-type widgets. This class has
16 functionality for:
16 functionality for:
17
17
18 * Maintaining a prompt and editing region
18 * Maintaining a prompt and editing region
19 * Providing the traditional Unix-style console keyboard shortcuts
19 * Providing the traditional Unix-style console keyboard shortcuts
20 * Performing tab completion
20 * Performing tab completion
21 * Paging text
21 * Paging text
22 * Handling ANSI escape codes
22 * Handling ANSI escape codes
23
23
24 ConsoleWidget also provides a number of utility methods that will be
24 ConsoleWidget also provides a number of utility methods that will be
25 convenient to implementors of a console-style widget.
25 convenient to implementors of a console-style widget.
26 """
26 """
27
27
28 # Whether to process ANSI escape codes.
28 # Whether to process ANSI escape codes.
29 ansi_codes = True
29 ansi_codes = True
30
30
31 # The maximum number of lines of text before truncation.
31 # The maximum number of lines of text before truncation.
32 buffer_size = 500
32 buffer_size = 500
33
33
34 # Whether to use a list widget or plain text output for tab completion.
34 # Whether to use a list widget or plain text output for tab completion.
35 gui_completion = True
35 gui_completion = True
36
36
37 # Whether to override ShortcutEvents for the keybindings defined by this
37 # Whether to override ShortcutEvents for the keybindings defined by this
38 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
38 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
39 # priority (when it has focus) over, e.g., window-level menu shortcuts.
39 # priority (when it has focus) over, e.g., window-level menu shortcuts.
40 override_shortcuts = False
40 override_shortcuts = False
41
41
42 # Signals that indicate ConsoleWidget state.
42 # Signals that indicate ConsoleWidget state.
43 copy_available = QtCore.pyqtSignal(bool)
43 copy_available = QtCore.pyqtSignal(bool)
44 redo_available = QtCore.pyqtSignal(bool)
44 redo_available = QtCore.pyqtSignal(bool)
45 undo_available = QtCore.pyqtSignal(bool)
45 undo_available = QtCore.pyqtSignal(bool)
46
46
47 # Signal emitted when paging is needed and the paging style has been
47 # Signal emitted when paging is needed and the paging style has been
48 # specified as 'custom'.
48 # specified as 'custom'.
49 custom_page_requested = QtCore.pyqtSignal(object)
49 custom_page_requested = QtCore.pyqtSignal(object)
50
50
51 # Protected class variables.
51 # Protected class variables.
52 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
52 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
53 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
53 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
54 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
54 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
55 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
55 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
56 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
56 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
57 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
57 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
58 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
58 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
59 _shortcuts = set(_ctrl_down_remap.keys() +
59 _shortcuts = set(_ctrl_down_remap.keys() +
60 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
60 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
61
61
62 #---------------------------------------------------------------------------
62 #---------------------------------------------------------------------------
63 # 'QObject' interface
63 # 'QObject' interface
64 #---------------------------------------------------------------------------
64 #---------------------------------------------------------------------------
65
65
66 def __init__(self, kind='plain', paging='inside', parent=None):
66 def __init__(self, kind='plain', paging='inside', parent=None):
67 """ Create a ConsoleWidget.
67 """ Create a ConsoleWidget.
68
68
69 Parameters
69 Parameters
70 ----------
70 ----------
71 kind : str, optional [default 'plain']
71 kind : str, optional [default 'plain']
72 The type of underlying text widget to use. Valid values are 'plain',
72 The type of underlying text widget to use. Valid values are 'plain',
73 which specifies a QPlainTextEdit, and 'rich', which specifies a
73 which specifies a QPlainTextEdit, and 'rich', which specifies a
74 QTextEdit.
74 QTextEdit.
75
75
76 paging : str, optional [default 'inside']
76 paging : str, optional [default 'inside']
77 The type of paging to use. Valid values are:
77 The type of paging to use. Valid values are:
78 'inside' : The widget pages like a traditional terminal pager.
78 'inside' : The widget pages like a traditional terminal pager.
79 'hsplit' : When paging is requested, the widget is split
79 'hsplit' : When paging is requested, the widget is split
80 horizontally. The top pane contains the console,
80 horizontally. The top pane contains the console,
81 and the bottom pane contains the paged text.
81 and the bottom pane contains the paged text.
82 'vsplit' : Similar to 'hsplit', except that a vertical splitter
82 'vsplit' : Similar to 'hsplit', except that a vertical splitter
83 used.
83 used.
84 'custom' : No action is taken by the widget beyond emitting a
84 'custom' : No action is taken by the widget beyond emitting a
85 'custom_page_requested(str)' signal.
85 'custom_page_requested(str)' signal.
86 'none' : The text is written directly to the console.
86 'none' : The text is written directly to the console.
87
87
88 parent : QWidget, optional [default None]
88 parent : QWidget, optional [default None]
89 The parent for this widget.
89 The parent for this widget.
90 """
90 """
91 super(ConsoleWidget, self).__init__(parent)
91 super(ConsoleWidget, self).__init__(parent)
92
92
93 # Create the layout and underlying text widget.
93 # Create the layout and underlying text widget.
94 layout = QtGui.QStackedLayout(self)
94 layout = QtGui.QStackedLayout(self)
95 layout.setMargin(0)
95 layout.setMargin(0)
96 self._control = self._create_control(kind)
96 self._control = self._create_control(kind)
97 self._page_control = None
97 self._page_control = None
98 self._splitter = None
98 self._splitter = None
99 if paging in ('hsplit', 'vsplit'):
99 if paging in ('hsplit', 'vsplit'):
100 self._splitter = QtGui.QSplitter()
100 self._splitter = QtGui.QSplitter()
101 if paging == 'hsplit':
101 if paging == 'hsplit':
102 self._splitter.setOrientation(QtCore.Qt.Horizontal)
102 self._splitter.setOrientation(QtCore.Qt.Horizontal)
103 else:
103 else:
104 self._splitter.setOrientation(QtCore.Qt.Vertical)
104 self._splitter.setOrientation(QtCore.Qt.Vertical)
105 self._splitter.addWidget(self._control)
105 self._splitter.addWidget(self._control)
106 layout.addWidget(self._splitter)
106 layout.addWidget(self._splitter)
107 else:
107 else:
108 layout.addWidget(self._control)
108 layout.addWidget(self._control)
109
109
110 # Create the paging widget, if necessary.
110 # Create the paging widget, if necessary.
111 self._page_style = paging
111 self._page_style = paging
112 if paging in ('inside', 'hsplit', 'vsplit'):
112 if paging in ('inside', 'hsplit', 'vsplit'):
113 self._page_control = self._create_page_control()
113 self._page_control = self._create_page_control()
114 if self._splitter:
114 if self._splitter:
115 self._page_control.hide()
115 self._page_control.hide()
116 self._splitter.addWidget(self._page_control)
116 self._splitter.addWidget(self._page_control)
117 else:
117 else:
118 layout.addWidget(self._page_control)
118 layout.addWidget(self._page_control)
119 elif paging not in ('custom', 'none'):
119 elif paging not in ('custom', 'none'):
120 raise ValueError('Paging style %s unknown.' % repr(paging))
120 raise ValueError('Paging style %s unknown.' % repr(paging))
121
121
122 # Initialize protected variables. Some variables contain useful state
122 # Initialize protected variables. Some variables contain useful state
123 # information for subclasses; they should be considered read-only.
123 # information for subclasses; they should be considered read-only.
124 self._ansi_processor = QtAnsiCodeProcessor()
124 self._ansi_processor = QtAnsiCodeProcessor()
125 self._completion_widget = CompletionWidget(self._control)
125 self._completion_widget = CompletionWidget(self._control)
126 self._continuation_prompt = '> '
126 self._continuation_prompt = '> '
127 self._continuation_prompt_html = None
127 self._continuation_prompt_html = None
128 self._executing = False
128 self._executing = False
129 self._prompt = ''
129 self._prompt = ''
130 self._prompt_html = None
130 self._prompt_html = None
131 self._prompt_pos = 0
131 self._prompt_pos = 0
132 self._reading = False
132 self._reading = False
133 self._reading_callback = None
133 self._reading_callback = None
134 self._tab_width = 8
134 self._tab_width = 8
135
135
136 # Set a monospaced font.
136 # Set a monospaced font.
137 self.reset_font()
137 self.reset_font()
138
138
139 def eventFilter(self, obj, event):
139 def eventFilter(self, obj, event):
140 """ Reimplemented to ensure a console-like behavior in the underlying
140 """ Reimplemented to ensure a console-like behavior in the underlying
141 text widget.
141 text widget.
142 """
142 """
143 # Re-map keys for all filtered widgets.
143 # Re-map keys for all filtered widgets.
144 etype = event.type()
144 etype = event.type()
145 if etype == QtCore.QEvent.KeyPress and \
145 if etype == QtCore.QEvent.KeyPress and \
146 self._control_key_down(event.modifiers()) and \
146 self._control_key_down(event.modifiers()) and \
147 event.key() in self._ctrl_down_remap:
147 event.key() in self._ctrl_down_remap:
148 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
148 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
149 self._ctrl_down_remap[event.key()],
149 self._ctrl_down_remap[event.key()],
150 QtCore.Qt.NoModifier)
150 QtCore.Qt.NoModifier)
151 QtGui.qApp.sendEvent(obj, new_event)
151 QtGui.qApp.sendEvent(obj, new_event)
152 return True
152 return True
153
153
154 # Override shortucts for all filtered widgets. Note that on Mac OS it is
154 # Override shortucts for all filtered widgets. Note that on Mac OS it is
155 # always unnecessary to override shortcuts, hence the check below (users
155 # always unnecessary to override shortcuts, hence the check below (users
156 # should just use the Control key instead of the Command key).
156 # should just use the Control key instead of the Command key).
157 elif etype == QtCore.QEvent.ShortcutOverride and \
157 elif etype == QtCore.QEvent.ShortcutOverride and \
158 sys.platform != 'darwin' and \
158 sys.platform != 'darwin' and \
159 self._control_key_down(event.modifiers()) and \
159 self._control_key_down(event.modifiers()) and \
160 event.key() in self._shortcuts:
160 event.key() in self._shortcuts:
161 event.accept()
161 event.accept()
162 return False
162 return False
163
163
164 elif obj == self._control:
164 elif obj == self._control:
165 # Disable moving text by drag and drop.
165 # Disable moving text by drag and drop.
166 if etype == QtCore.QEvent.DragMove:
166 if etype == QtCore.QEvent.DragMove:
167 return True
167 return True
168
168
169 elif etype == QtCore.QEvent.KeyPress:
169 elif etype == QtCore.QEvent.KeyPress:
170 return self._event_filter_console_keypress(event)
170 return self._event_filter_console_keypress(event)
171
171
172 elif obj == self._page_control:
172 elif obj == self._page_control:
173 if etype == QtCore.QEvent.KeyPress:
173 if etype == QtCore.QEvent.KeyPress:
174 return self._event_filter_page_keypress(event)
174 return self._event_filter_page_keypress(event)
175
175
176 return super(ConsoleWidget, self).eventFilter(obj, event)
176 return super(ConsoleWidget, self).eventFilter(obj, event)
177
177
178 #---------------------------------------------------------------------------
178 #---------------------------------------------------------------------------
179 # 'QWidget' interface
179 # 'QWidget' interface
180 #---------------------------------------------------------------------------
180 #---------------------------------------------------------------------------
181
181
182 def sizeHint(self):
182 def sizeHint(self):
183 """ Reimplemented to suggest a size that is 80 characters wide and
183 """ Reimplemented to suggest a size that is 80 characters wide and
184 25 lines high.
184 25 lines high.
185 """
185 """
186 style = self.style()
186 style = self.style()
187 opt = QtGui.QStyleOptionHeader()
187 opt = QtGui.QStyleOptionHeader()
188 font_metrics = QtGui.QFontMetrics(self.font)
188 font_metrics = QtGui.QFontMetrics(self.font)
189 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
189 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
190
190
191 width = font_metrics.width(' ') * 80
191 width = font_metrics.width(' ') * 80
192 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
192 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
193 if self._page_style == 'hsplit':
193 if self._page_style == 'hsplit':
194 width = width * 2 + splitwidth
194 width = width * 2 + splitwidth
195
195
196 height = font_metrics.height() * 25
196 height = font_metrics.height() * 25
197 if self._page_style == 'vsplit':
197 if self._page_style == 'vsplit':
198 height = height * 2 + splitwidth
198 height = height * 2 + splitwidth
199
199
200 return QtCore.QSize(width, height)
200 return QtCore.QSize(width, height)
201
201
202 #---------------------------------------------------------------------------
202 #---------------------------------------------------------------------------
203 # 'ConsoleWidget' public interface
203 # 'ConsoleWidget' public interface
204 #---------------------------------------------------------------------------
204 #---------------------------------------------------------------------------
205
205
206 def can_paste(self):
206 def can_paste(self):
207 """ Returns whether text can be pasted from the clipboard.
207 """ Returns whether text can be pasted from the clipboard.
208 """
208 """
209 # Accept only text that can be ASCII encoded.
209 # Accept only text that can be ASCII encoded.
210 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
210 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
211 text = QtGui.QApplication.clipboard().text()
211 text = QtGui.QApplication.clipboard().text()
212 if not text.isEmpty():
212 if not text.isEmpty():
213 try:
213 try:
214 str(text)
214 str(text)
215 return True
215 return True
216 except UnicodeEncodeError:
216 except UnicodeEncodeError:
217 pass
217 pass
218 return False
218 return False
219
219
220 def clear(self, keep_input=True):
220 def clear(self, keep_input=True):
221 """ Clear the console, then write a new prompt. If 'keep_input' is set,
221 """ Clear the console, then write a new prompt. If 'keep_input' is set,
222 restores the old input buffer when the new prompt is written.
222 restores the old input buffer when the new prompt is written.
223 """
223 """
224 if keep_input:
224 if keep_input:
225 input_buffer = self.input_buffer
225 input_buffer = self.input_buffer
226 self._control.clear()
226 self._control.clear()
227 self._show_prompt()
227 self._show_prompt()
228 if keep_input:
228 if keep_input:
229 self.input_buffer = input_buffer
229 self.input_buffer = input_buffer
230
230
231 def copy(self):
231 def copy(self):
232 """ Copy the current selected text to the clipboard.
232 """ Copy the current selected text to the clipboard.
233 """
233 """
234 self._control.copy()
234 self._control.copy()
235
235
236 def execute(self, source=None, hidden=False, interactive=False):
236 def execute(self, source=None, hidden=False, interactive=False):
237 """ Executes source or the input buffer, possibly prompting for more
237 """ Executes source or the input buffer, possibly prompting for more
238 input.
238 input.
239
239
240 Parameters:
240 Parameters:
241 -----------
241 -----------
242 source : str, optional
242 source : str, optional
243
243
244 The source to execute. If not specified, the input buffer will be
244 The source to execute. If not specified, the input buffer will be
245 used. If specified and 'hidden' is False, the input buffer will be
245 used. If specified and 'hidden' is False, the input buffer will be
246 replaced with the source before execution.
246 replaced with the source before execution.
247
247
248 hidden : bool, optional (default False)
248 hidden : bool, optional (default False)
249
249
250 If set, no output will be shown and the prompt will not be modified.
250 If set, no output will be shown and the prompt will not be modified.
251 In other words, it will be completely invisible to the user that
251 In other words, it will be completely invisible to the user that
252 an execution has occurred.
252 an execution has occurred.
253
253
254 interactive : bool, optional (default False)
254 interactive : bool, optional (default False)
255
255
256 Whether the console is to treat the source as having been manually
256 Whether the console is to treat the source as having been manually
257 entered by the user. The effect of this parameter depends on the
257 entered by the user. The effect of this parameter depends on the
258 subclass implementation.
258 subclass implementation.
259
259
260 Raises:
260 Raises:
261 -------
261 -------
262 RuntimeError
262 RuntimeError
263 If incomplete input is given and 'hidden' is True. In this case,
263 If incomplete input is given and 'hidden' is True. In this case,
264 it is not possible to prompt for more input.
264 it is not possible to prompt for more input.
265
265
266 Returns:
266 Returns:
267 --------
267 --------
268 A boolean indicating whether the source was executed.
268 A boolean indicating whether the source was executed.
269 """
269 """
270 if not hidden:
270 if not hidden:
271 if source is not None:
271 if source is not None:
272 self.input_buffer = source
272 self.input_buffer = source
273
273
274 self._append_plain_text('\n')
274 self._append_plain_text('\n')
275 self._executing_input_buffer = self.input_buffer
275 self._executing_input_buffer = self.input_buffer
276 self._executing = True
276 self._executing = True
277 self._prompt_finished()
277 self._prompt_finished()
278
278
279 real_source = self.input_buffer if source is None else source
279 real_source = self.input_buffer if source is None else source
280 complete = self._is_complete(real_source, interactive)
280 complete = self._is_complete(real_source, interactive)
281 if complete:
281 if complete:
282 if not hidden:
282 if not hidden:
283 # The maximum block count is only in effect during execution.
283 # The maximum block count is only in effect during execution.
284 # This ensures that _prompt_pos does not become invalid due to
284 # This ensures that _prompt_pos does not become invalid due to
285 # text truncation.
285 # text truncation.
286 self._control.document().setMaximumBlockCount(self.buffer_size)
286 self._control.document().setMaximumBlockCount(self.buffer_size)
287 self._execute(real_source, hidden)
287 self._execute(real_source, hidden)
288 elif hidden:
288 elif hidden:
289 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
289 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
290 else:
290 else:
291 self._show_continuation_prompt()
291 self._show_continuation_prompt()
292
292
293 return complete
293 return complete
294
294
295 def _get_input_buffer(self):
295 def _get_input_buffer(self):
296 """ The text that the user has entered entered at the current prompt.
296 """ The text that the user has entered entered at the current prompt.
297 """
297 """
298 # If we're executing, the input buffer may not even exist anymore due to
298 # If we're executing, the input buffer may not even exist anymore due to
299 # the limit imposed by 'buffer_size'. Therefore, we store it.
299 # the limit imposed by 'buffer_size'. Therefore, we store it.
300 if self._executing:
300 if self._executing:
301 return self._executing_input_buffer
301 return self._executing_input_buffer
302
302
303 cursor = self._get_end_cursor()
303 cursor = self._get_end_cursor()
304 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
304 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
305 input_buffer = str(cursor.selection().toPlainText())
305 input_buffer = str(cursor.selection().toPlainText())
306
306
307 # Strip out continuation prompts.
307 # Strip out continuation prompts.
308 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
308 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
309
309
310 def _set_input_buffer(self, string):
310 def _set_input_buffer(self, string):
311 """ Replaces the text in the input buffer with 'string'.
311 """ Replaces the text in the input buffer with 'string'.
312 """
312 """
313 # For now, it is an error to modify the input buffer during execution.
313 # For now, it is an error to modify the input buffer during execution.
314 if self._executing:
314 if self._executing:
315 raise RuntimeError("Cannot change input buffer during execution.")
315 raise RuntimeError("Cannot change input buffer during execution.")
316
316
317 # Remove old text.
317 # Remove old text.
318 cursor = self._get_end_cursor()
318 cursor = self._get_end_cursor()
319 cursor.beginEditBlock()
319 cursor.beginEditBlock()
320 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
320 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
321 cursor.removeSelectedText()
321 cursor.removeSelectedText()
322
322
323 # Insert new text with continuation prompts.
323 # Insert new text with continuation prompts.
324 lines = string.splitlines(True)
324 lines = string.splitlines(True)
325 if lines:
325 if lines:
326 self._append_plain_text(lines[0])
326 self._append_plain_text(lines[0])
327 for i in xrange(1, len(lines)):
327 for i in xrange(1, len(lines)):
328 if self._continuation_prompt_html is None:
328 if self._continuation_prompt_html is None:
329 self._append_plain_text(self._continuation_prompt)
329 self._append_plain_text(self._continuation_prompt)
330 else:
330 else:
331 self._append_html(self._continuation_prompt_html)
331 self._append_html(self._continuation_prompt_html)
332 self._append_plain_text(lines[i])
332 self._append_plain_text(lines[i])
333 cursor.endEditBlock()
333 cursor.endEditBlock()
334 self._control.moveCursor(QtGui.QTextCursor.End)
334 self._control.moveCursor(QtGui.QTextCursor.End)
335
335
336 input_buffer = property(_get_input_buffer, _set_input_buffer)
336 input_buffer = property(_get_input_buffer, _set_input_buffer)
337
337
338 def _get_font(self):
338 def _get_font(self):
339 """ The base font being used by the ConsoleWidget.
339 """ The base font being used by the ConsoleWidget.
340 """
340 """
341 return self._control.document().defaultFont()
341 return self._control.document().defaultFont()
342
342
343 def _set_font(self, font):
343 def _set_font(self, font):
344 """ Sets the base font for the ConsoleWidget to the specified QFont.
344 """ Sets the base font for the ConsoleWidget to the specified QFont.
345 """
345 """
346 font_metrics = QtGui.QFontMetrics(font)
346 font_metrics = QtGui.QFontMetrics(font)
347 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
347 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
348
348
349 self._completion_widget.setFont(font)
349 self._completion_widget.setFont(font)
350 self._control.document().setDefaultFont(font)
350 self._control.document().setDefaultFont(font)
351 if self._page_control:
351 if self._page_control:
352 self._page_control.document().setDefaultFont(font)
352 self._page_control.document().setDefaultFont(font)
353
353
354 font = property(_get_font, _set_font)
354 font = property(_get_font, _set_font)
355
355
356 def paste(self):
356 def paste(self):
357 """ Paste the contents of the clipboard into the input region.
357 """ Paste the contents of the clipboard into the input region.
358 """
358 """
359 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
359 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
360 try:
360 try:
361 text = str(QtGui.QApplication.clipboard().text())
361 text = str(QtGui.QApplication.clipboard().text())
362 except UnicodeEncodeError:
362 except UnicodeEncodeError:
363 pass
363 pass
364 else:
364 else:
365 self._insert_plain_text_into_buffer(dedent(text))
365 self._insert_plain_text_into_buffer(dedent(text))
366
366
367 def print_(self, printer):
367 def print_(self, printer):
368 """ Print the contents of the ConsoleWidget to the specified QPrinter.
368 """ Print the contents of the ConsoleWidget to the specified QPrinter.
369 """
369 """
370 self._control.print_(printer)
370 self._control.print_(printer)
371
371
372 def redo(self):
372 def redo(self):
373 """ Redo the last operation. If there is no operation to redo, nothing
373 """ Redo the last operation. If there is no operation to redo, nothing
374 happens.
374 happens.
375 """
375 """
376 self._control.redo()
376 self._control.redo()
377
377
378 def reset_font(self):
378 def reset_font(self):
379 """ Sets the font to the default fixed-width font for this platform.
379 """ Sets the font to the default fixed-width font for this platform.
380 """
380 """
381 if sys.platform == 'win32':
381 if sys.platform == 'win32':
382 name = 'Courier'
382 name = 'Courier'
383 elif sys.platform == 'darwin':
383 elif sys.platform == 'darwin':
384 name = 'Monaco'
384 name = 'Monaco'
385 else:
385 else:
386 name = 'Monospace'
386 name = 'Monospace'
387 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
387 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
388 font.setStyleHint(QtGui.QFont.TypeWriter)
388 font.setStyleHint(QtGui.QFont.TypeWriter)
389 self._set_font(font)
389 self._set_font(font)
390
390
391 def select_all(self):
391 def select_all(self):
392 """ Selects all the text in the buffer.
392 """ Selects all the text in the buffer.
393 """
393 """
394 self._control.selectAll()
394 self._control.selectAll()
395
395
396 def _get_tab_width(self):
396 def _get_tab_width(self):
397 """ The width (in terms of space characters) for tab characters.
397 """ The width (in terms of space characters) for tab characters.
398 """
398 """
399 return self._tab_width
399 return self._tab_width
400
400
401 def _set_tab_width(self, tab_width):
401 def _set_tab_width(self, tab_width):
402 """ Sets the width (in terms of space characters) for tab characters.
402 """ Sets the width (in terms of space characters) for tab characters.
403 """
403 """
404 font_metrics = QtGui.QFontMetrics(self.font)
404 font_metrics = QtGui.QFontMetrics(self.font)
405 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
405 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
406
406
407 self._tab_width = tab_width
407 self._tab_width = tab_width
408
408
409 tab_width = property(_get_tab_width, _set_tab_width)
409 tab_width = property(_get_tab_width, _set_tab_width)
410
410
411 def undo(self):
411 def undo(self):
412 """ Undo the last operation. If there is no operation to undo, nothing
412 """ Undo the last operation. If there is no operation to undo, nothing
413 happens.
413 happens.
414 """
414 """
415 self._control.undo()
415 self._control.undo()
416
416
417 #---------------------------------------------------------------------------
417 #---------------------------------------------------------------------------
418 # 'ConsoleWidget' abstract interface
418 # 'ConsoleWidget' abstract interface
419 #---------------------------------------------------------------------------
419 #---------------------------------------------------------------------------
420
420
421 def _is_complete(self, source, interactive):
421 def _is_complete(self, source, interactive):
422 """ Returns whether 'source' can be executed. When triggered by an
422 """ Returns whether 'source' can be executed. When triggered by an
423 Enter/Return key press, 'interactive' is True; otherwise, it is
423 Enter/Return key press, 'interactive' is True; otherwise, it is
424 False.
424 False.
425 """
425 """
426 raise NotImplementedError
426 raise NotImplementedError
427
427
428 def _execute(self, source, hidden):
428 def _execute(self, source, hidden):
429 """ Execute 'source'. If 'hidden', do not show any output.
429 """ Execute 'source'. If 'hidden', do not show any output.
430 """
430 """
431 raise NotImplementedError
431 raise NotImplementedError
432
432
433 def _execute_interrupt(self):
433 def _execute_interrupt(self):
434 """ Attempts to stop execution. Returns whether this method has an
434 """ Attempts to stop execution. Returns whether this method has an
435 implementation.
435 implementation.
436 """
436 """
437 return False
437 return False
438
438
439 def _prompt_started_hook(self):
439 def _prompt_started_hook(self):
440 """ Called immediately after a new prompt is displayed.
440 """ Called immediately after a new prompt is displayed.
441 """
441 """
442 pass
442 pass
443
443
444 def _prompt_finished_hook(self):
444 def _prompt_finished_hook(self):
445 """ Called immediately after a prompt is finished, i.e. when some input
445 """ Called immediately after a prompt is finished, i.e. when some input
446 will be processed and a new prompt displayed.
446 will be processed and a new prompt displayed.
447 """
447 """
448 pass
448 pass
449
449
450 def _up_pressed(self):
450 def _up_pressed(self):
451 """ Called when the up key is pressed. Returns whether to continue
451 """ Called when the up key is pressed. Returns whether to continue
452 processing the event.
452 processing the event.
453 """
453 """
454 return True
454 return True
455
455
456 def _down_pressed(self):
456 def _down_pressed(self):
457 """ Called when the down key is pressed. Returns whether to continue
457 """ Called when the down key is pressed. Returns whether to continue
458 processing the event.
458 processing the event.
459 """
459 """
460 return True
460 return True
461
461
462 def _tab_pressed(self):
462 def _tab_pressed(self):
463 """ Called when the tab key is pressed. Returns whether to continue
463 """ Called when the tab key is pressed. Returns whether to continue
464 processing the event.
464 processing the event.
465 """
465 """
466 return False
466 return False
467
467
468 #--------------------------------------------------------------------------
468 #--------------------------------------------------------------------------
469 # 'ConsoleWidget' protected interface
469 # 'ConsoleWidget' protected interface
470 #--------------------------------------------------------------------------
470 #--------------------------------------------------------------------------
471
471
472 def _append_html(self, html):
472 def _append_html(self, html):
473 """ Appends html at the end of the console buffer.
473 """ Appends html at the end of the console buffer.
474 """
474 """
475 cursor = self._get_end_cursor()
475 cursor = self._get_end_cursor()
476 self._insert_html(cursor, html)
476 self._insert_html(cursor, html)
477
477
478 def _append_html_fetching_plain_text(self, html):
478 def _append_html_fetching_plain_text(self, html):
479 """ Appends 'html', then returns the plain text version of it.
479 """ Appends 'html', then returns the plain text version of it.
480 """
480 """
481 cursor = self._get_end_cursor()
481 cursor = self._get_end_cursor()
482 return self._insert_html_fetching_plain_text(cursor, html)
482 return self._insert_html_fetching_plain_text(cursor, html)
483
483
484 def _append_plain_text(self, text):
484 def _append_plain_text(self, text):
485 """ Appends plain text at the end of the console buffer, processing
485 """ Appends plain text at the end of the console buffer, processing
486 ANSI codes if enabled.
486 ANSI codes if enabled.
487 """
487 """
488 cursor = self._get_end_cursor()
488 cursor = self._get_end_cursor()
489 self._insert_plain_text(cursor, text)
489 self._insert_plain_text(cursor, text)
490
490
491 def _append_plain_text_keeping_prompt(self, text):
491 def _append_plain_text_keeping_prompt(self, text):
492 """ Writes 'text' after the current prompt, then restores the old prompt
492 """ Writes 'text' after the current prompt, then restores the old prompt
493 with its old input buffer.
493 with its old input buffer.
494 """
494 """
495 input_buffer = self.input_buffer
495 input_buffer = self.input_buffer
496 self._append_plain_text('\n')
496 self._append_plain_text('\n')
497 self._prompt_finished()
497 self._prompt_finished()
498
498
499 self._append_plain_text(text)
499 self._append_plain_text(text)
500 self._show_prompt()
500 self._show_prompt()
501 self.input_buffer = input_buffer
501 self.input_buffer = input_buffer
502
502
503 def _complete_with_items(self, cursor, items):
503 def _complete_with_items(self, cursor, items):
504 """ Performs completion with 'items' at the specified cursor location.
504 """ Performs completion with 'items' at the specified cursor location.
505 """
505 """
506 if len(items) == 1:
506 if len(items) == 1:
507 cursor.setPosition(self._control.textCursor().position(),
507 cursor.setPosition(self._control.textCursor().position(),
508 QtGui.QTextCursor.KeepAnchor)
508 QtGui.QTextCursor.KeepAnchor)
509 cursor.insertText(items[0])
509 cursor.insertText(items[0])
510 elif len(items) > 1:
510 elif len(items) > 1:
511 if self.gui_completion:
511 if self.gui_completion:
512 self._completion_widget.show_items(cursor, items)
512 self._completion_widget.show_items(cursor, items)
513 else:
513 else:
514 text = self._format_as_columns(items)
514 text = self._format_as_columns(items)
515 self._append_plain_text_keeping_prompt(text)
515 self._append_plain_text_keeping_prompt(text)
516
516
517 def _control_key_down(self, modifiers):
517 def _control_key_down(self, modifiers):
518 """ Given a KeyboardModifiers flags object, return whether the Control
518 """ Given a KeyboardModifiers flags object, return whether the Control
519 key is down (on Mac OS, treat the Command key as a synonym for
519 key is down (on Mac OS, treat the Command key as a synonym for
520 Control).
520 Control).
521 """
521 """
522 down = bool(modifiers & QtCore.Qt.ControlModifier)
522 down = bool(modifiers & QtCore.Qt.ControlModifier)
523
523
524 # Note: on Mac OS, ControlModifier corresponds to the Command key while
524 # Note: on Mac OS, ControlModifier corresponds to the Command key while
525 # MetaModifier corresponds to the Control key.
525 # MetaModifier corresponds to the Control key.
526 if sys.platform == 'darwin':
526 if sys.platform == 'darwin':
527 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
527 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
528
528
529 return down
529 return down
530
530
531 def _create_control(self, kind):
531 def _create_control(self, kind):
532 """ Creates and connects the underlying text widget.
532 """ Creates and connects the underlying text widget.
533 """
533 """
534 if kind == 'plain':
534 if kind == 'plain':
535 control = QtGui.QPlainTextEdit()
535 control = QtGui.QPlainTextEdit()
536 elif kind == 'rich':
536 elif kind == 'rich':
537 control = QtGui.QTextEdit()
537 control = QtGui.QTextEdit()
538 control.setAcceptRichText(False)
538 control.setAcceptRichText(False)
539 else:
539 else:
540 raise ValueError("Kind %s unknown." % repr(kind))
540 raise ValueError("Kind %s unknown." % repr(kind))
541 control.installEventFilter(self)
541 control.installEventFilter(self)
542 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
542 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
543 control.customContextMenuRequested.connect(self._show_context_menu)
543 control.customContextMenuRequested.connect(self._show_context_menu)
544 control.copyAvailable.connect(self.copy_available)
544 control.copyAvailable.connect(self.copy_available)
545 control.redoAvailable.connect(self.redo_available)
545 control.redoAvailable.connect(self.redo_available)
546 control.undoAvailable.connect(self.undo_available)
546 control.undoAvailable.connect(self.undo_available)
547 control.setReadOnly(True)
547 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
548 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
548 return control
549 return control
549
550
550 def _create_page_control(self):
551 def _create_page_control(self):
551 """ Creates and connects the underlying paging widget.
552 """ Creates and connects the underlying paging widget.
552 """
553 """
553 control = QtGui.QPlainTextEdit()
554 control = QtGui.QPlainTextEdit()
554 control.installEventFilter(self)
555 control.installEventFilter(self)
555 control.setReadOnly(True)
556 control.setReadOnly(True)
556 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
557 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
557 return control
558 return control
558
559
559 def _event_filter_console_keypress(self, event):
560 def _event_filter_console_keypress(self, event):
560 """ Filter key events for the underlying text widget to create a
561 """ Filter key events for the underlying text widget to create a
561 console-like interface.
562 console-like interface.
562 """
563 """
563 intercepted = False
564 intercepted = False
564 cursor = self._control.textCursor()
565 cursor = self._control.textCursor()
565 position = cursor.position()
566 position = cursor.position()
566 key = event.key()
567 key = event.key()
567 ctrl_down = self._control_key_down(event.modifiers())
568 ctrl_down = self._control_key_down(event.modifiers())
568 alt_down = event.modifiers() & QtCore.Qt.AltModifier
569 alt_down = event.modifiers() & QtCore.Qt.AltModifier
569 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
570 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
570
571
571 if event.matches(QtGui.QKeySequence.Paste):
572 if event.matches(QtGui.QKeySequence.Paste):
572 # Call our paste instead of the underlying text widget's.
573 # Call our paste instead of the underlying text widget's.
573 self.paste()
574 self.paste()
574 intercepted = True
575 intercepted = True
575
576
576 elif ctrl_down:
577 elif ctrl_down:
577 if key == QtCore.Qt.Key_C:
578 if key == QtCore.Qt.Key_C:
578 intercepted = self._executing and self._execute_interrupt()
579 intercepted = self._executing and self._execute_interrupt()
579
580
580 elif key == QtCore.Qt.Key_K:
581 elif key == QtCore.Qt.Key_K:
581 if self._in_buffer(position):
582 if self._in_buffer(position):
582 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
583 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
583 QtGui.QTextCursor.KeepAnchor)
584 QtGui.QTextCursor.KeepAnchor)
584 cursor.removeSelectedText()
585 cursor.removeSelectedText()
585 intercepted = True
586 intercepted = True
586
587
587 elif key == QtCore.Qt.Key_X:
588 elif key == QtCore.Qt.Key_X:
588 intercepted = True
589 intercepted = True
589
590
590 elif key == QtCore.Qt.Key_Y:
591 elif key == QtCore.Qt.Key_Y:
591 self.paste()
592 self.paste()
592 intercepted = True
593 intercepted = True
593
594
594 elif alt_down:
595 elif alt_down:
595 if key == QtCore.Qt.Key_B:
596 if key == QtCore.Qt.Key_B:
596 self._set_cursor(self._get_word_start_cursor(position))
597 self._set_cursor(self._get_word_start_cursor(position))
597 intercepted = True
598 intercepted = True
598
599
599 elif key == QtCore.Qt.Key_F:
600 elif key == QtCore.Qt.Key_F:
600 self._set_cursor(self._get_word_end_cursor(position))
601 self._set_cursor(self._get_word_end_cursor(position))
601 intercepted = True
602 intercepted = True
602
603
603 elif key == QtCore.Qt.Key_Backspace:
604 elif key == QtCore.Qt.Key_Backspace:
604 cursor = self._get_word_start_cursor(position)
605 cursor = self._get_word_start_cursor(position)
605 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
606 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
606 cursor.removeSelectedText()
607 cursor.removeSelectedText()
607 intercepted = True
608 intercepted = True
608
609
609 elif key == QtCore.Qt.Key_D:
610 elif key == QtCore.Qt.Key_D:
610 cursor = self._get_word_end_cursor(position)
611 cursor = self._get_word_end_cursor(position)
611 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
612 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
612 cursor.removeSelectedText()
613 cursor.removeSelectedText()
613 intercepted = True
614 intercepted = True
614
615
615 else:
616 else:
616 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
617 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
617 if self._reading:
618 if self._reading:
618 self._append_plain_text('\n')
619 self._append_plain_text('\n')
619 self._reading = False
620 self._reading = False
620 if self._reading_callback:
621 if self._reading_callback:
621 self._reading_callback()
622 self._reading_callback()
622 elif not self._executing:
623 elif not self._executing:
623 self.execute(interactive=True)
624 self.execute(interactive=True)
624 intercepted = True
625 intercepted = True
625
626
626 elif key == QtCore.Qt.Key_Up:
627 elif key == QtCore.Qt.Key_Up:
627 if self._reading or not self._up_pressed():
628 if self._reading or not self._up_pressed():
628 intercepted = True
629 intercepted = True
629 else:
630 else:
630 prompt_line = self._get_prompt_cursor().blockNumber()
631 prompt_line = self._get_prompt_cursor().blockNumber()
631 intercepted = cursor.blockNumber() <= prompt_line
632 intercepted = cursor.blockNumber() <= prompt_line
632
633
633 elif key == QtCore.Qt.Key_Down:
634 elif key == QtCore.Qt.Key_Down:
634 if self._reading or not self._down_pressed():
635 if self._reading or not self._down_pressed():
635 intercepted = True
636 intercepted = True
636 else:
637 else:
637 end_line = self._get_end_cursor().blockNumber()
638 end_line = self._get_end_cursor().blockNumber()
638 intercepted = cursor.blockNumber() == end_line
639 intercepted = cursor.blockNumber() == end_line
639
640
640 elif key == QtCore.Qt.Key_Tab:
641 elif key == QtCore.Qt.Key_Tab:
641 if self._reading:
642 if self._reading:
642 intercepted = False
643 intercepted = False
643 else:
644 else:
644 intercepted = not self._tab_pressed()
645 intercepted = not self._tab_pressed()
645
646
646 elif key == QtCore.Qt.Key_Left:
647 elif key == QtCore.Qt.Key_Left:
647 intercepted = not self._in_buffer(position - 1)
648 intercepted = not self._in_buffer(position - 1)
648
649
649 elif key == QtCore.Qt.Key_Home:
650 elif key == QtCore.Qt.Key_Home:
650 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
651 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
651 start_line = cursor.blockNumber()
652 start_line = cursor.blockNumber()
652 if start_line == self._get_prompt_cursor().blockNumber():
653 if start_line == self._get_prompt_cursor().blockNumber():
653 start_pos = self._prompt_pos
654 start_pos = self._prompt_pos
654 else:
655 else:
655 start_pos = cursor.position()
656 start_pos = cursor.position()
656 start_pos += len(self._continuation_prompt)
657 start_pos += len(self._continuation_prompt)
657 if shift_down and self._in_buffer(position):
658 if shift_down and self._in_buffer(position):
658 self._set_selection(position, start_pos)
659 self._set_selection(position, start_pos)
659 else:
660 else:
660 self._set_position(start_pos)
661 self._set_position(start_pos)
661 intercepted = True
662 intercepted = True
662
663
663 elif key == QtCore.Qt.Key_Backspace:
664 elif key == QtCore.Qt.Key_Backspace:
664
665
665 # Line deletion (remove continuation prompt)
666 # Line deletion (remove continuation prompt)
666 len_prompt = len(self._continuation_prompt)
667 len_prompt = len(self._continuation_prompt)
667 if not self._reading and \
668 if not self._reading and \
668 cursor.columnNumber() == len_prompt and \
669 cursor.columnNumber() == len_prompt and \
669 position != self._prompt_pos:
670 position != self._prompt_pos:
670 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
671 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
671 QtGui.QTextCursor.KeepAnchor)
672 QtGui.QTextCursor.KeepAnchor)
672 cursor.removeSelectedText()
673 cursor.removeSelectedText()
673 cursor.deletePreviousChar()
674 cursor.deletePreviousChar()
674 intercepted = True
675 intercepted = True
675
676
676 # Regular backwards deletion
677 # Regular backwards deletion
677 else:
678 else:
678 anchor = cursor.anchor()
679 anchor = cursor.anchor()
679 if anchor == position:
680 if anchor == position:
680 intercepted = not self._in_buffer(position - 1)
681 intercepted = not self._in_buffer(position - 1)
681 else:
682 else:
682 intercepted = not self._in_buffer(min(anchor, position))
683 intercepted = not self._in_buffer(min(anchor, position))
683
684
684 elif key == QtCore.Qt.Key_Delete:
685 elif key == QtCore.Qt.Key_Delete:
685 anchor = cursor.anchor()
686 anchor = cursor.anchor()
686 intercepted = not self._in_buffer(min(anchor, position))
687 intercepted = not self._in_buffer(min(anchor, position))
687
688
688 # Don't move the cursor if control is down to allow copy-paste using
689 # Don't move the cursor if control is down to allow copy-paste using
689 # the keyboard in any part of the buffer.
690 # the keyboard in any part of the buffer.
690 if not ctrl_down:
691 if not ctrl_down:
691 self._keep_cursor_in_buffer()
692 self._keep_cursor_in_buffer()
692
693
693 return intercepted
694 return intercepted
694
695
695 def _event_filter_page_keypress(self, event):
696 def _event_filter_page_keypress(self, event):
696 """ Filter key events for the paging widget to create console-like
697 """ Filter key events for the paging widget to create console-like
697 interface.
698 interface.
698 """
699 """
699 key = event.key()
700 key = event.key()
700
701
701 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
702 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
702 if self._splitter:
703 if self._splitter:
703 self._page_control.hide()
704 self._page_control.hide()
704 else:
705 else:
705 self.layout().setCurrentWidget(self._control)
706 self.layout().setCurrentWidget(self._control)
706 return True
707 return True
707
708
708 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
709 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
709 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
710 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
710 QtCore.Qt.Key_Down,
711 QtCore.Qt.Key_Down,
711 QtCore.Qt.NoModifier)
712 QtCore.Qt.NoModifier)
712 QtGui.qApp.sendEvent(self._page_control, new_event)
713 QtGui.qApp.sendEvent(self._page_control, new_event)
713 return True
714 return True
714
715
715 return False
716 return False
716
717
717 def _format_as_columns(self, items, separator=' '):
718 def _format_as_columns(self, items, separator=' '):
718 """ Transform a list of strings into a single string with columns.
719 """ Transform a list of strings into a single string with columns.
719
720
720 Parameters
721 Parameters
721 ----------
722 ----------
722 items : sequence of strings
723 items : sequence of strings
723 The strings to process.
724 The strings to process.
724
725
725 separator : str, optional [default is two spaces]
726 separator : str, optional [default is two spaces]
726 The string that separates columns.
727 The string that separates columns.
727
728
728 Returns
729 Returns
729 -------
730 -------
730 The formatted string.
731 The formatted string.
731 """
732 """
732 # Note: this code is adapted from columnize 0.3.2.
733 # Note: this code is adapted from columnize 0.3.2.
733 # See http://code.google.com/p/pycolumnize/
734 # See http://code.google.com/p/pycolumnize/
734
735
735 width = self._control.viewport().width()
736 width = self._control.viewport().width()
736 char_width = QtGui.QFontMetrics(self.font).width(' ')
737 char_width = QtGui.QFontMetrics(self.font).width(' ')
737 displaywidth = max(5, width / char_width)
738 displaywidth = max(5, width / char_width)
738
739
739 # Some degenerate cases.
740 # Some degenerate cases.
740 size = len(items)
741 size = len(items)
741 if size == 0:
742 if size == 0:
742 return '\n'
743 return '\n'
743 elif size == 1:
744 elif size == 1:
744 return '%s\n' % str(items[0])
745 return '%s\n' % str(items[0])
745
746
746 # Try every row count from 1 upwards
747 # Try every row count from 1 upwards
747 array_index = lambda nrows, row, col: nrows*col + row
748 array_index = lambda nrows, row, col: nrows*col + row
748 for nrows in range(1, size):
749 for nrows in range(1, size):
749 ncols = (size + nrows - 1) // nrows
750 ncols = (size + nrows - 1) // nrows
750 colwidths = []
751 colwidths = []
751 totwidth = -len(separator)
752 totwidth = -len(separator)
752 for col in range(ncols):
753 for col in range(ncols):
753 # Get max column width for this column
754 # Get max column width for this column
754 colwidth = 0
755 colwidth = 0
755 for row in range(nrows):
756 for row in range(nrows):
756 i = array_index(nrows, row, col)
757 i = array_index(nrows, row, col)
757 if i >= size: break
758 if i >= size: break
758 x = items[i]
759 x = items[i]
759 colwidth = max(colwidth, len(x))
760 colwidth = max(colwidth, len(x))
760 colwidths.append(colwidth)
761 colwidths.append(colwidth)
761 totwidth += colwidth + len(separator)
762 totwidth += colwidth + len(separator)
762 if totwidth > displaywidth:
763 if totwidth > displaywidth:
763 break
764 break
764 if totwidth <= displaywidth:
765 if totwidth <= displaywidth:
765 break
766 break
766
767
767 # The smallest number of rows computed and the max widths for each
768 # The smallest number of rows computed and the max widths for each
768 # column has been obtained. Now we just have to format each of the rows.
769 # column has been obtained. Now we just have to format each of the rows.
769 string = ''
770 string = ''
770 for row in range(nrows):
771 for row in range(nrows):
771 texts = []
772 texts = []
772 for col in range(ncols):
773 for col in range(ncols):
773 i = row + nrows*col
774 i = row + nrows*col
774 if i >= size:
775 if i >= size:
775 texts.append('')
776 texts.append('')
776 else:
777 else:
777 texts.append(items[i])
778 texts.append(items[i])
778 while texts and not texts[-1]:
779 while texts and not texts[-1]:
779 del texts[-1]
780 del texts[-1]
780 for col in range(len(texts)):
781 for col in range(len(texts)):
781 texts[col] = texts[col].ljust(colwidths[col])
782 texts[col] = texts[col].ljust(colwidths[col])
782 string += '%s\n' % str(separator.join(texts))
783 string += '%s\n' % str(separator.join(texts))
783 return string
784 return string
784
785
785 def _get_block_plain_text(self, block):
786 def _get_block_plain_text(self, block):
786 """ Given a QTextBlock, return its unformatted text.
787 """ Given a QTextBlock, return its unformatted text.
787 """
788 """
788 cursor = QtGui.QTextCursor(block)
789 cursor = QtGui.QTextCursor(block)
789 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
790 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
790 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
791 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
791 QtGui.QTextCursor.KeepAnchor)
792 QtGui.QTextCursor.KeepAnchor)
792 return str(cursor.selection().toPlainText())
793 return str(cursor.selection().toPlainText())
793
794
794 def _get_cursor(self):
795 def _get_cursor(self):
795 """ Convenience method that returns a cursor for the current position.
796 """ Convenience method that returns a cursor for the current position.
796 """
797 """
797 return self._control.textCursor()
798 return self._control.textCursor()
798
799
799 def _get_end_cursor(self):
800 def _get_end_cursor(self):
800 """ Convenience method that returns a cursor for the last character.
801 """ Convenience method that returns a cursor for the last character.
801 """
802 """
802 cursor = self._control.textCursor()
803 cursor = self._control.textCursor()
803 cursor.movePosition(QtGui.QTextCursor.End)
804 cursor.movePosition(QtGui.QTextCursor.End)
804 return cursor
805 return cursor
805
806
806 def _get_input_buffer_cursor_column(self):
807 def _get_input_buffer_cursor_column(self):
807 """ Returns the column of the cursor in the input buffer, excluding the
808 """ Returns the column of the cursor in the input buffer, excluding the
808 contribution by the prompt, or -1 if there is no such column.
809 contribution by the prompt, or -1 if there is no such column.
809 """
810 """
810 prompt = self._get_input_buffer_cursor_prompt()
811 prompt = self._get_input_buffer_cursor_prompt()
811 if prompt is None:
812 if prompt is None:
812 return -1
813 return -1
813 else:
814 else:
814 cursor = self._control.textCursor()
815 cursor = self._control.textCursor()
815 return cursor.columnNumber() - len(prompt)
816 return cursor.columnNumber() - len(prompt)
816
817
817 def _get_input_buffer_cursor_line(self):
818 def _get_input_buffer_cursor_line(self):
818 """ Returns line of the input buffer that contains the cursor, or None
819 """ Returns line of the input buffer that contains the cursor, or None
819 if there is no such line.
820 if there is no such line.
820 """
821 """
821 prompt = self._get_input_buffer_cursor_prompt()
822 prompt = self._get_input_buffer_cursor_prompt()
822 if prompt is None:
823 if prompt is None:
823 return None
824 return None
824 else:
825 else:
825 cursor = self._control.textCursor()
826 cursor = self._control.textCursor()
826 text = self._get_block_plain_text(cursor.block())
827 text = self._get_block_plain_text(cursor.block())
827 return text[len(prompt):]
828 return text[len(prompt):]
828
829
829 def _get_input_buffer_cursor_prompt(self):
830 def _get_input_buffer_cursor_prompt(self):
830 """ Returns the (plain text) prompt for line of the input buffer that
831 """ Returns the (plain text) prompt for line of the input buffer that
831 contains the cursor, or None if there is no such line.
832 contains the cursor, or None if there is no such line.
832 """
833 """
833 if self._executing:
834 if self._executing:
834 return None
835 return None
835 cursor = self._control.textCursor()
836 cursor = self._control.textCursor()
836 if cursor.position() >= self._prompt_pos:
837 if cursor.position() >= self._prompt_pos:
837 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
838 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
838 return self._prompt
839 return self._prompt
839 else:
840 else:
840 return self._continuation_prompt
841 return self._continuation_prompt
841 else:
842 else:
842 return None
843 return None
843
844
844 def _get_prompt_cursor(self):
845 def _get_prompt_cursor(self):
845 """ Convenience method that returns a cursor for the prompt position.
846 """ Convenience method that returns a cursor for the prompt position.
846 """
847 """
847 cursor = self._control.textCursor()
848 cursor = self._control.textCursor()
848 cursor.setPosition(self._prompt_pos)
849 cursor.setPosition(self._prompt_pos)
849 return cursor
850 return cursor
850
851
851 def _get_selection_cursor(self, start, end):
852 def _get_selection_cursor(self, start, end):
852 """ Convenience method that returns a cursor with text selected between
853 """ Convenience method that returns a cursor with text selected between
853 the positions 'start' and 'end'.
854 the positions 'start' and 'end'.
854 """
855 """
855 cursor = self._control.textCursor()
856 cursor = self._control.textCursor()
856 cursor.setPosition(start)
857 cursor.setPosition(start)
857 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
858 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
858 return cursor
859 return cursor
859
860
860 def _get_word_start_cursor(self, position):
861 def _get_word_start_cursor(self, position):
861 """ Find the start of the word to the left the given position. If a
862 """ Find the start of the word to the left the given position. If a
862 sequence of non-word characters precedes the first word, skip over
863 sequence of non-word characters precedes the first word, skip over
863 them. (This emulates the behavior of bash, emacs, etc.)
864 them. (This emulates the behavior of bash, emacs, etc.)
864 """
865 """
865 document = self._control.document()
866 document = self._control.document()
866 position -= 1
867 position -= 1
867 while position >= self._prompt_pos and \
868 while position >= self._prompt_pos and \
868 not document.characterAt(position).isLetterOrNumber():
869 not document.characterAt(position).isLetterOrNumber():
869 position -= 1
870 position -= 1
870 while position >= self._prompt_pos and \
871 while position >= self._prompt_pos and \
871 document.characterAt(position).isLetterOrNumber():
872 document.characterAt(position).isLetterOrNumber():
872 position -= 1
873 position -= 1
873 cursor = self._control.textCursor()
874 cursor = self._control.textCursor()
874 cursor.setPosition(position + 1)
875 cursor.setPosition(position + 1)
875 return cursor
876 return cursor
876
877
877 def _get_word_end_cursor(self, position):
878 def _get_word_end_cursor(self, position):
878 """ Find the end of the word to the right the given position. If a
879 """ Find the end of the word to the right the given position. If a
879 sequence of non-word characters precedes the first word, skip over
880 sequence of non-word characters precedes the first word, skip over
880 them. (This emulates the behavior of bash, emacs, etc.)
881 them. (This emulates the behavior of bash, emacs, etc.)
881 """
882 """
882 document = self._control.document()
883 document = self._control.document()
883 end = self._get_end_cursor().position()
884 end = self._get_end_cursor().position()
884 while position < end and \
885 while position < end and \
885 not document.characterAt(position).isLetterOrNumber():
886 not document.characterAt(position).isLetterOrNumber():
886 position += 1
887 position += 1
887 while position < end and \
888 while position < end and \
888 document.characterAt(position).isLetterOrNumber():
889 document.characterAt(position).isLetterOrNumber():
889 position += 1
890 position += 1
890 cursor = self._control.textCursor()
891 cursor = self._control.textCursor()
891 cursor.setPosition(position)
892 cursor.setPosition(position)
892 return cursor
893 return cursor
893
894
894 def _insert_html(self, cursor, html):
895 def _insert_html(self, cursor, html):
895 """ Inserts HTML using the specified cursor in such a way that future
896 """ Inserts HTML using the specified cursor in such a way that future
896 formatting is unaffected.
897 formatting is unaffected.
897 """
898 """
898 cursor.beginEditBlock()
899 cursor.beginEditBlock()
899 cursor.insertHtml(html)
900 cursor.insertHtml(html)
900
901
901 # After inserting HTML, the text document "remembers" it's in "html
902 # After inserting HTML, the text document "remembers" it's in "html
902 # mode", which means that subsequent calls adding plain text will result
903 # mode", which means that subsequent calls adding plain text will result
903 # in unwanted formatting, lost tab characters, etc. The following code
904 # in unwanted formatting, lost tab characters, etc. The following code
904 # hacks around this behavior, which I consider to be a bug in Qt, by
905 # hacks around this behavior, which I consider to be a bug in Qt, by
905 # (crudely) resetting the document's style state.
906 # (crudely) resetting the document's style state.
906 cursor.movePosition(QtGui.QTextCursor.Left,
907 cursor.movePosition(QtGui.QTextCursor.Left,
907 QtGui.QTextCursor.KeepAnchor)
908 QtGui.QTextCursor.KeepAnchor)
908 if cursor.selection().toPlainText() == ' ':
909 if cursor.selection().toPlainText() == ' ':
909 cursor.removeSelectedText()
910 cursor.removeSelectedText()
910 else:
911 else:
911 cursor.movePosition(QtGui.QTextCursor.Right)
912 cursor.movePosition(QtGui.QTextCursor.Right)
912 cursor.insertText(' ', QtGui.QTextCharFormat())
913 cursor.insertText(' ', QtGui.QTextCharFormat())
913 cursor.endEditBlock()
914 cursor.endEditBlock()
914
915
915 def _insert_html_fetching_plain_text(self, cursor, html):
916 def _insert_html_fetching_plain_text(self, cursor, html):
916 """ Inserts HTML using the specified cursor, then returns its plain text
917 """ Inserts HTML using the specified cursor, then returns its plain text
917 version.
918 version.
918 """
919 """
919 cursor.beginEditBlock()
920 cursor.beginEditBlock()
920 cursor.removeSelectedText()
921 cursor.removeSelectedText()
921
922
922 start = cursor.position()
923 start = cursor.position()
923 self._insert_html(cursor, html)
924 self._insert_html(cursor, html)
924 end = cursor.position()
925 end = cursor.position()
925 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
926 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
926 text = str(cursor.selection().toPlainText())
927 text = str(cursor.selection().toPlainText())
927
928
928 cursor.setPosition(end)
929 cursor.setPosition(end)
929 cursor.endEditBlock()
930 cursor.endEditBlock()
930 return text
931 return text
931
932
932 def _insert_plain_text(self, cursor, text):
933 def _insert_plain_text(self, cursor, text):
933 """ Inserts plain text using the specified cursor, processing ANSI codes
934 """ Inserts plain text using the specified cursor, processing ANSI codes
934 if enabled.
935 if enabled.
935 """
936 """
936 cursor.beginEditBlock()
937 cursor.beginEditBlock()
937 if self.ansi_codes:
938 if self.ansi_codes:
938 for substring in self._ansi_processor.split_string(text):
939 for substring in self._ansi_processor.split_string(text):
939 for action in self._ansi_processor.actions:
940 for action in self._ansi_processor.actions:
940 if action.kind == 'erase' and action.area == 'screen':
941 if action.kind == 'erase' and action.area == 'screen':
941 cursor.select(QtGui.QTextCursor.Document)
942 cursor.select(QtGui.QTextCursor.Document)
942 cursor.removeSelectedText()
943 cursor.removeSelectedText()
943 format = self._ansi_processor.get_format()
944 format = self._ansi_processor.get_format()
944 cursor.insertText(substring, format)
945 cursor.insertText(substring, format)
945 else:
946 else:
946 cursor.insertText(text)
947 cursor.insertText(text)
947 cursor.endEditBlock()
948 cursor.endEditBlock()
948
949
949 def _insert_plain_text_into_buffer(self, text):
950 def _insert_plain_text_into_buffer(self, text):
950 """ Inserts text into the input buffer at the current cursor position,
951 """ Inserts text into the input buffer at the current cursor position,
951 ensuring that continuation prompts are inserted as necessary.
952 ensuring that continuation prompts are inserted as necessary.
952 """
953 """
953 lines = str(text).splitlines(True)
954 lines = str(text).splitlines(True)
954 if lines:
955 if lines:
955 self._keep_cursor_in_buffer()
956 self._keep_cursor_in_buffer()
956 cursor = self._control.textCursor()
957 cursor = self._control.textCursor()
957 cursor.beginEditBlock()
958 cursor.beginEditBlock()
958 cursor.insertText(lines[0])
959 cursor.insertText(lines[0])
959 for line in lines[1:]:
960 for line in lines[1:]:
960 if self._continuation_prompt_html is None:
961 if self._continuation_prompt_html is None:
961 cursor.insertText(self._continuation_prompt)
962 cursor.insertText(self._continuation_prompt)
962 else:
963 else:
963 self._continuation_prompt = \
964 self._continuation_prompt = \
964 self._insert_html_fetching_plain_text(
965 self._insert_html_fetching_plain_text(
965 cursor, self._continuation_prompt_html)
966 cursor, self._continuation_prompt_html)
966 cursor.insertText(line)
967 cursor.insertText(line)
967 cursor.endEditBlock()
968 cursor.endEditBlock()
968 self._control.setTextCursor(cursor)
969 self._control.setTextCursor(cursor)
969
970
970 def _in_buffer(self, position):
971 def _in_buffer(self, position):
971 """ Returns whether the given position is inside the editing region.
972 """ Returns whether the given position is inside the editing region.
972 """
973 """
973 cursor = self._control.textCursor()
974 cursor = self._control.textCursor()
974 cursor.setPosition(position)
975 cursor.setPosition(position)
975 line = cursor.blockNumber()
976 line = cursor.blockNumber()
976 prompt_line = self._get_prompt_cursor().blockNumber()
977 prompt_line = self._get_prompt_cursor().blockNumber()
977 if line == prompt_line:
978 if line == prompt_line:
978 return position >= self._prompt_pos
979 return position >= self._prompt_pos
979 elif line > prompt_line:
980 elif line > prompt_line:
980 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
981 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
981 prompt_pos = cursor.position() + len(self._continuation_prompt)
982 prompt_pos = cursor.position() + len(self._continuation_prompt)
982 return position >= prompt_pos
983 return position >= prompt_pos
983 return False
984 return False
984
985
985 def _keep_cursor_in_buffer(self):
986 def _keep_cursor_in_buffer(self):
986 """ Ensures that the cursor is inside the editing region. Returns
987 """ Ensures that the cursor is inside the editing region. Returns
987 whether the cursor was moved.
988 whether the cursor was moved.
988 """
989 """
989 cursor = self._control.textCursor()
990 cursor = self._control.textCursor()
990 if self._in_buffer(cursor.position()):
991 if self._in_buffer(cursor.position()):
991 return False
992 return False
992 else:
993 else:
993 cursor.movePosition(QtGui.QTextCursor.End)
994 cursor.movePosition(QtGui.QTextCursor.End)
994 self._control.setTextCursor(cursor)
995 self._control.setTextCursor(cursor)
995 return True
996 return True
996
997
997 def _page(self, text):
998 def _page(self, text):
998 """ Displays text using the pager if it exceeds the height of the
999 """ Displays text using the pager if it exceeds the height of the
999 visible area.
1000 visible area.
1000 """
1001 """
1001 if self._page_style == 'none':
1002 if self._page_style == 'none':
1002 self._append_plain_text(text)
1003 self._append_plain_text(text)
1003 else:
1004 else:
1004 line_height = QtGui.QFontMetrics(self.font).height()
1005 line_height = QtGui.QFontMetrics(self.font).height()
1005 minlines = self._control.viewport().height() / line_height
1006 minlines = self._control.viewport().height() / line_height
1006 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1007 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1007 if self._page_style == 'custom':
1008 if self._page_style == 'custom':
1008 self.custom_page_requested.emit(text)
1009 self.custom_page_requested.emit(text)
1009 else:
1010 else:
1010 self._page_control.clear()
1011 self._page_control.clear()
1011 cursor = self._page_control.textCursor()
1012 cursor = self._page_control.textCursor()
1012 self._insert_plain_text(cursor, text)
1013 self._insert_plain_text(cursor, text)
1013 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1014 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1014
1015
1015 self._page_control.viewport().resize(self._control.size())
1016 self._page_control.viewport().resize(self._control.size())
1016 if self._splitter:
1017 if self._splitter:
1017 self._page_control.show()
1018 self._page_control.show()
1018 self._page_control.setFocus()
1019 self._page_control.setFocus()
1019 else:
1020 else:
1020 self.layout().setCurrentWidget(self._page_control)
1021 self.layout().setCurrentWidget(self._page_control)
1021 else:
1022 else:
1022 self._append_plain_text(text)
1023 self._append_plain_text(text)
1023
1024
1024 def _prompt_started(self):
1025 def _prompt_started(self):
1025 """ Called immediately after a new prompt is displayed.
1026 """ Called immediately after a new prompt is displayed.
1026 """
1027 """
1027 # Temporarily disable the maximum block count to permit undo/redo and
1028 # Temporarily disable the maximum block count to permit undo/redo and
1028 # to ensure that the prompt position does not change due to truncation.
1029 # to ensure that the prompt position does not change due to truncation.
1029 self._control.document().setMaximumBlockCount(0)
1030 self._control.document().setMaximumBlockCount(0)
1030 self._control.setUndoRedoEnabled(True)
1031 self._control.setUndoRedoEnabled(True)
1031
1032
1032 self._control.setReadOnly(False)
1033 self._control.setReadOnly(False)
1033 self._control.moveCursor(QtGui.QTextCursor.End)
1034 self._control.moveCursor(QtGui.QTextCursor.End)
1034
1035
1035 self._executing = False
1036 self._executing = False
1036 self._prompt_started_hook()
1037 self._prompt_started_hook()
1037
1038
1038 def _prompt_finished(self):
1039 def _prompt_finished(self):
1039 """ Called immediately after a prompt is finished, i.e. when some input
1040 """ Called immediately after a prompt is finished, i.e. when some input
1040 will be processed and a new prompt displayed.
1041 will be processed and a new prompt displayed.
1041 """
1042 """
1042 self._control.setUndoRedoEnabled(False)
1043 self._control.setUndoRedoEnabled(False)
1043 self._control.setReadOnly(True)
1044 self._control.setReadOnly(True)
1044 self._prompt_finished_hook()
1045 self._prompt_finished_hook()
1045
1046
1046 def _readline(self, prompt='', callback=None):
1047 def _readline(self, prompt='', callback=None):
1047 """ Reads one line of input from the user.
1048 """ Reads one line of input from the user.
1048
1049
1049 Parameters
1050 Parameters
1050 ----------
1051 ----------
1051 prompt : str, optional
1052 prompt : str, optional
1052 The prompt to print before reading the line.
1053 The prompt to print before reading the line.
1053
1054
1054 callback : callable, optional
1055 callback : callable, optional
1055 A callback to execute with the read line. If not specified, input is
1056 A callback to execute with the read line. If not specified, input is
1056 read *synchronously* and this method does not return until it has
1057 read *synchronously* and this method does not return until it has
1057 been read.
1058 been read.
1058
1059
1059 Returns
1060 Returns
1060 -------
1061 -------
1061 If a callback is specified, returns nothing. Otherwise, returns the
1062 If a callback is specified, returns nothing. Otherwise, returns the
1062 input string with the trailing newline stripped.
1063 input string with the trailing newline stripped.
1063 """
1064 """
1064 if self._reading:
1065 if self._reading:
1065 raise RuntimeError('Cannot read a line. Widget is already reading.')
1066 raise RuntimeError('Cannot read a line. Widget is already reading.')
1066
1067
1067 if not callback and not self.isVisible():
1068 if not callback and not self.isVisible():
1068 # If the user cannot see the widget, this function cannot return.
1069 # If the user cannot see the widget, this function cannot return.
1069 raise RuntimeError('Cannot synchronously read a line if the widget'
1070 raise RuntimeError('Cannot synchronously read a line if the widget'
1070 'is not visible!')
1071 'is not visible!')
1071
1072
1072 self._reading = True
1073 self._reading = True
1073 self._show_prompt(prompt, newline=False)
1074 self._show_prompt(prompt, newline=False)
1074
1075
1075 if callback is None:
1076 if callback is None:
1076 self._reading_callback = None
1077 self._reading_callback = None
1077 while self._reading:
1078 while self._reading:
1078 QtCore.QCoreApplication.processEvents()
1079 QtCore.QCoreApplication.processEvents()
1079 return self.input_buffer.rstrip('\n')
1080 return self.input_buffer.rstrip('\n')
1080
1081
1081 else:
1082 else:
1082 self._reading_callback = lambda: \
1083 self._reading_callback = lambda: \
1083 callback(self.input_buffer.rstrip('\n'))
1084 callback(self.input_buffer.rstrip('\n'))
1084
1085
1085 def _set_continuation_prompt(self, prompt, html=False):
1086 def _set_continuation_prompt(self, prompt, html=False):
1086 """ Sets the continuation prompt.
1087 """ Sets the continuation prompt.
1087
1088
1088 Parameters
1089 Parameters
1089 ----------
1090 ----------
1090 prompt : str
1091 prompt : str
1091 The prompt to show when more input is needed.
1092 The prompt to show when more input is needed.
1092
1093
1093 html : bool, optional (default False)
1094 html : bool, optional (default False)
1094 If set, the prompt will be inserted as formatted HTML. Otherwise,
1095 If set, the prompt will be inserted as formatted HTML. Otherwise,
1095 the prompt will be treated as plain text, though ANSI color codes
1096 the prompt will be treated as plain text, though ANSI color codes
1096 will be handled.
1097 will be handled.
1097 """
1098 """
1098 if html:
1099 if html:
1099 self._continuation_prompt_html = prompt
1100 self._continuation_prompt_html = prompt
1100 else:
1101 else:
1101 self._continuation_prompt = prompt
1102 self._continuation_prompt = prompt
1102 self._continuation_prompt_html = None
1103 self._continuation_prompt_html = None
1103
1104
1104 def _set_cursor(self, cursor):
1105 def _set_cursor(self, cursor):
1105 """ Convenience method to set the current cursor.
1106 """ Convenience method to set the current cursor.
1106 """
1107 """
1107 self._control.setTextCursor(cursor)
1108 self._control.setTextCursor(cursor)
1108
1109
1109 def _set_position(self, position):
1110 def _set_position(self, position):
1110 """ Convenience method to set the position of the cursor.
1111 """ Convenience method to set the position of the cursor.
1111 """
1112 """
1112 cursor = self._control.textCursor()
1113 cursor = self._control.textCursor()
1113 cursor.setPosition(position)
1114 cursor.setPosition(position)
1114 self._control.setTextCursor(cursor)
1115 self._control.setTextCursor(cursor)
1115
1116
1116 def _set_selection(self, start, end):
1117 def _set_selection(self, start, end):
1117 """ Convenience method to set the current selected text.
1118 """ Convenience method to set the current selected text.
1118 """
1119 """
1119 self._control.setTextCursor(self._get_selection_cursor(start, end))
1120 self._control.setTextCursor(self._get_selection_cursor(start, end))
1120
1121
1121 def _show_context_menu(self, pos):
1122 def _show_context_menu(self, pos):
1122 """ Shows a context menu at the given QPoint (in widget coordinates).
1123 """ Shows a context menu at the given QPoint (in widget coordinates).
1123 """
1124 """
1124 menu = QtGui.QMenu()
1125 menu = QtGui.QMenu()
1125
1126
1126 copy_action = menu.addAction('Copy', self.copy)
1127 copy_action = menu.addAction('Copy', self.copy)
1127 copy_action.setEnabled(self._get_cursor().hasSelection())
1128 copy_action.setEnabled(self._get_cursor().hasSelection())
1128 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1129 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1129
1130
1130 paste_action = menu.addAction('Paste', self.paste)
1131 paste_action = menu.addAction('Paste', self.paste)
1131 paste_action.setEnabled(self.can_paste())
1132 paste_action.setEnabled(self.can_paste())
1132 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1133 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1133
1134
1134 menu.addSeparator()
1135 menu.addSeparator()
1135 menu.addAction('Select All', self.select_all)
1136 menu.addAction('Select All', self.select_all)
1136
1137
1137 menu.exec_(self._control.mapToGlobal(pos))
1138 menu.exec_(self._control.mapToGlobal(pos))
1138
1139
1139 def _show_prompt(self, prompt=None, html=False, newline=True):
1140 def _show_prompt(self, prompt=None, html=False, newline=True):
1140 """ Writes a new prompt at the end of the buffer.
1141 """ Writes a new prompt at the end of the buffer.
1141
1142
1142 Parameters
1143 Parameters
1143 ----------
1144 ----------
1144 prompt : str, optional
1145 prompt : str, optional
1145 The prompt to show. If not specified, the previous prompt is used.
1146 The prompt to show. If not specified, the previous prompt is used.
1146
1147
1147 html : bool, optional (default False)
1148 html : bool, optional (default False)
1148 Only relevant when a prompt is specified. If set, the prompt will
1149 Only relevant when a prompt is specified. If set, the prompt will
1149 be inserted as formatted HTML. Otherwise, the prompt will be treated
1150 be inserted as formatted HTML. Otherwise, the prompt will be treated
1150 as plain text, though ANSI color codes will be handled.
1151 as plain text, though ANSI color codes will be handled.
1151
1152
1152 newline : bool, optional (default True)
1153 newline : bool, optional (default True)
1153 If set, a new line will be written before showing the prompt if
1154 If set, a new line will be written before showing the prompt if
1154 there is not already a newline at the end of the buffer.
1155 there is not already a newline at the end of the buffer.
1155 """
1156 """
1156 # Insert a preliminary newline, if necessary.
1157 # Insert a preliminary newline, if necessary.
1157 if newline:
1158 if newline:
1158 cursor = self._get_end_cursor()
1159 cursor = self._get_end_cursor()
1159 if cursor.position() > 0:
1160 if cursor.position() > 0:
1160 cursor.movePosition(QtGui.QTextCursor.Left,
1161 cursor.movePosition(QtGui.QTextCursor.Left,
1161 QtGui.QTextCursor.KeepAnchor)
1162 QtGui.QTextCursor.KeepAnchor)
1162 if str(cursor.selection().toPlainText()) != '\n':
1163 if str(cursor.selection().toPlainText()) != '\n':
1163 self._append_plain_text('\n')
1164 self._append_plain_text('\n')
1164
1165
1165 # Write the prompt.
1166 # Write the prompt.
1166 if prompt is None:
1167 if prompt is None:
1167 if self._prompt_html is None:
1168 if self._prompt_html is None:
1168 self._append_plain_text(self._prompt)
1169 self._append_plain_text(self._prompt)
1169 else:
1170 else:
1170 self._append_html(self._prompt_html)
1171 self._append_html(self._prompt_html)
1171 else:
1172 else:
1172 if html:
1173 if html:
1173 self._prompt = self._append_html_fetching_plain_text(prompt)
1174 self._prompt = self._append_html_fetching_plain_text(prompt)
1174 self._prompt_html = prompt
1175 self._prompt_html = prompt
1175 else:
1176 else:
1176 self._append_plain_text(prompt)
1177 self._append_plain_text(prompt)
1177 self._prompt = prompt
1178 self._prompt = prompt
1178 self._prompt_html = None
1179 self._prompt_html = None
1179
1180
1180 self._prompt_pos = self._get_end_cursor().position()
1181 self._prompt_pos = self._get_end_cursor().position()
1181 self._prompt_started()
1182 self._prompt_started()
1182
1183
1183 def _show_continuation_prompt(self):
1184 def _show_continuation_prompt(self):
1184 """ Writes a new continuation prompt at the end of the buffer.
1185 """ Writes a new continuation prompt at the end of the buffer.
1185 """
1186 """
1186 if self._continuation_prompt_html is None:
1187 if self._continuation_prompt_html is None:
1187 self._append_plain_text(self._continuation_prompt)
1188 self._append_plain_text(self._continuation_prompt)
1188 else:
1189 else:
1189 self._continuation_prompt = self._append_html_fetching_plain_text(
1190 self._continuation_prompt = self._append_html_fetching_plain_text(
1190 self._continuation_prompt_html)
1191 self._continuation_prompt_html)
1191
1192
1192 self._prompt_started()
1193 self._prompt_started()
1193
1194
1194
1195
1195 class HistoryConsoleWidget(ConsoleWidget):
1196 class HistoryConsoleWidget(ConsoleWidget):
1196 """ A ConsoleWidget that keeps a history of the commands that have been
1197 """ A ConsoleWidget that keeps a history of the commands that have been
1197 executed.
1198 executed.
1198 """
1199 """
1199
1200
1200 #---------------------------------------------------------------------------
1201 #---------------------------------------------------------------------------
1201 # 'object' interface
1202 # 'object' interface
1202 #---------------------------------------------------------------------------
1203 #---------------------------------------------------------------------------
1203
1204
1204 def __init__(self, *args, **kw):
1205 def __init__(self, *args, **kw):
1205 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1206 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1206 self._history = []
1207 self._history = []
1207 self._history_index = 0
1208 self._history_index = 0
1208
1209
1209 #---------------------------------------------------------------------------
1210 #---------------------------------------------------------------------------
1210 # 'ConsoleWidget' public interface
1211 # 'ConsoleWidget' public interface
1211 #---------------------------------------------------------------------------
1212 #---------------------------------------------------------------------------
1212
1213
1213 def execute(self, source=None, hidden=False, interactive=False):
1214 def execute(self, source=None, hidden=False, interactive=False):
1214 """ Reimplemented to the store history.
1215 """ Reimplemented to the store history.
1215 """
1216 """
1216 if not hidden:
1217 if not hidden:
1217 history = self.input_buffer if source is None else source
1218 history = self.input_buffer if source is None else source
1218
1219
1219 executed = super(HistoryConsoleWidget, self).execute(
1220 executed = super(HistoryConsoleWidget, self).execute(
1220 source, hidden, interactive)
1221 source, hidden, interactive)
1221
1222
1222 if executed and not hidden:
1223 if executed and not hidden:
1223 self._history.append(history.rstrip())
1224 self._history.append(history.rstrip())
1224 self._history_index = len(self._history)
1225 self._history_index = len(self._history)
1225
1226
1226 return executed
1227 return executed
1227
1228
1228 #---------------------------------------------------------------------------
1229 #---------------------------------------------------------------------------
1229 # 'ConsoleWidget' abstract interface
1230 # 'ConsoleWidget' abstract interface
1230 #---------------------------------------------------------------------------
1231 #---------------------------------------------------------------------------
1231
1232
1232 def _up_pressed(self):
1233 def _up_pressed(self):
1233 """ Called when the up key is pressed. Returns whether to continue
1234 """ Called when the up key is pressed. Returns whether to continue
1234 processing the event.
1235 processing the event.
1235 """
1236 """
1236 prompt_cursor = self._get_prompt_cursor()
1237 prompt_cursor = self._get_prompt_cursor()
1237 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1238 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1238 self.history_previous()
1239 self.history_previous()
1239
1240
1240 # Go to the first line of prompt for seemless history scrolling.
1241 # Go to the first line of prompt for seemless history scrolling.
1241 cursor = self._get_prompt_cursor()
1242 cursor = self._get_prompt_cursor()
1242 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1243 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1243 self._set_cursor(cursor)
1244 self._set_cursor(cursor)
1244
1245
1245 return False
1246 return False
1246 return True
1247 return True
1247
1248
1248 def _down_pressed(self):
1249 def _down_pressed(self):
1249 """ Called when the down key is pressed. Returns whether to continue
1250 """ Called when the down key is pressed. Returns whether to continue
1250 processing the event.
1251 processing the event.
1251 """
1252 """
1252 end_cursor = self._get_end_cursor()
1253 end_cursor = self._get_end_cursor()
1253 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1254 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1254 self.history_next()
1255 self.history_next()
1255 return False
1256 return False
1256 return True
1257 return True
1257
1258
1258 #---------------------------------------------------------------------------
1259 #---------------------------------------------------------------------------
1259 # 'HistoryConsoleWidget' interface
1260 # 'HistoryConsoleWidget' public interface
1260 #---------------------------------------------------------------------------
1261 #---------------------------------------------------------------------------
1261
1262
1262 def history_previous(self):
1263 def history_previous(self):
1263 """ If possible, set the input buffer to the previous item in the
1264 """ If possible, set the input buffer to the previous item in the
1264 history.
1265 history.
1265 """
1266 """
1266 if self._history_index > 0:
1267 if self._history_index > 0:
1267 self._history_index -= 1
1268 self._history_index -= 1
1268 self.input_buffer = self._history[self._history_index]
1269 self.input_buffer = self._history[self._history_index]
1269
1270
1270 def history_next(self):
1271 def history_next(self):
1271 """ Set the input buffer to the next item in the history, or a blank
1272 """ Set the input buffer to the next item in the history, or a blank
1272 line if there is no subsequent item.
1273 line if there is no subsequent item.
1273 """
1274 """
1274 if self._history_index < len(self._history):
1275 if self._history_index < len(self._history):
1275 self._history_index += 1
1276 self._history_index += 1
1276 if self._history_index < len(self._history):
1277 if self._history_index < len(self._history):
1277 self.input_buffer = self._history[self._history_index]
1278 self.input_buffer = self._history[self._history_index]
1278 else:
1279 else:
1279 self.input_buffer = ''
1280 self.input_buffer = ''
1281
1282 #---------------------------------------------------------------------------
1283 # 'HistoryConsoleWidget' protected interface
1284 #---------------------------------------------------------------------------
1285
1286 def _set_history(self, history):
1287 """ Replace the current history with a sequence of history items.
1288 """
1289 self._history = list(history)
1290 self._history_index = len(self._history)
@@ -1,379 +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, hidden)
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._control.clear()
242 self._control.clear()
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 self._executing = self._reading = False
250 self._executing = self._reading = False
251 self._highlighter.highlighting_on = False
251 self._highlighter.highlighting_on = False
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 self._complete_id = self.kernel_manager.xreq_channel.complete(
295 self._complete_id = self.kernel_manager.xreq_channel.complete(
296 '.'.join(context), # text
296 '.'.join(context), # text
297 self._get_input_buffer_cursor_line(), # line
297 self._get_input_buffer_cursor_line(), # line
298 self._get_input_buffer_cursor_column(), # cursor_pos
298 self._get_input_buffer_cursor_column(), # cursor_pos
299 self.input_buffer) # block
299 self.input_buffer) # block
300 self._complete_pos = self._get_cursor().position()
300 self._complete_pos = self._get_cursor().position()
301 return True
301 return True
302
302
303 def _get_banner(self):
303 def _get_banner(self):
304 """ Gets a banner to display at the beginning of a session.
304 """ Gets a banner to display at the beginning of a session.
305 """
305 """
306 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
306 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
307 '"license" for more information.'
307 '"license" for more information.'
308 return banner % (sys.version, sys.platform)
308 return banner % (sys.version, sys.platform)
309
309
310 def _get_context(self, cursor=None):
310 def _get_context(self, cursor=None):
311 """ Gets the context at the current cursor location.
311 """ Gets the context at the current cursor location.
312 """
312 """
313 if cursor is None:
313 if cursor is None:
314 cursor = self._get_cursor()
314 cursor = self._get_cursor()
315 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
315 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
316 QtGui.QTextCursor.KeepAnchor)
316 QtGui.QTextCursor.KeepAnchor)
317 text = str(cursor.selection().toPlainText())
317 text = str(cursor.selection().toPlainText())
318 return self._completion_lexer.get_context(text)
318 return self._completion_lexer.get_context(text)
319
319
320 def _interrupt_kernel(self):
320 def _interrupt_kernel(self):
321 """ Attempts to the interrupt the kernel.
321 """ Attempts to the interrupt the kernel.
322 """
322 """
323 if self.kernel_manager.has_kernel:
323 if self.kernel_manager.has_kernel:
324 self.kernel_manager.signal_kernel(signal.SIGINT)
324 self.kernel_manager.signal_kernel(signal.SIGINT)
325 else:
325 else:
326 self._append_plain_text('Kernel process is either remote or '
326 self._append_plain_text('Kernel process is either remote or '
327 'unspecified. Cannot interrupt.\n')
327 'unspecified. Cannot interrupt.\n')
328
328
329 def _process_execute_abort(self, msg):
329 def _process_execute_abort(self, msg):
330 """ Process a reply for an aborted execution request.
330 """ Process a reply for an aborted execution request.
331 """
331 """
332 self._append_plain_text("ERROR: execution aborted\n")
332 self._append_plain_text("ERROR: execution aborted\n")
333
333
334 def _process_execute_error(self, msg):
334 def _process_execute_error(self, msg):
335 """ 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.
336 """
336 """
337 content = msg['content']
337 content = msg['content']
338 traceback = ''.join(content['traceback'])
338 traceback = ''.join(content['traceback'])
339 self._append_plain_text(traceback)
339 self._append_plain_text(traceback)
340
340
341 def _process_execute_ok(self, msg):
341 def _process_execute_ok(self, msg):
342 """ Process a reply for a successful execution equest.
342 """ Process a reply for a successful execution equest.
343 """
343 """
344 payload = msg['content']['payload']
344 payload = msg['content']['payload']
345 for item in payload:
345 for item in payload:
346 if not self._process_execute_payload(item):
346 if not self._process_execute_payload(item):
347 warning = 'Received unknown payload of type %s\n'
347 warning = 'Received unknown payload of type %s\n'
348 self._append_plain_text(warning % repr(item['source']))
348 self._append_plain_text(warning % repr(item['source']))
349
349
350 def _process_execute_payload(self, item):
350 def _process_execute_payload(self, item):
351 """ 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
352 execution reply. Returns whether the payload was handled.
352 execution reply. Returns whether the payload was handled.
353 """
353 """
354 # The basic FrontendWidget doesn't handle payloads, as they are a
354 # The basic FrontendWidget doesn't handle payloads, as they are a
355 # mechanism for going beyond the standard Python interpreter model.
355 # mechanism for going beyond the standard Python interpreter model.
356 return False
356 return False
357
357
358 def _show_interpreter_prompt(self):
358 def _show_interpreter_prompt(self):
359 """ Shows a prompt for the interpreter.
359 """ Shows a prompt for the interpreter.
360 """
360 """
361 self._show_prompt('>>> ')
361 self._show_prompt('>>> ')
362
362
363 def _show_interpreter_prompt_for_reply(self, msg):
363 def _show_interpreter_prompt_for_reply(self, msg):
364 """ Shows a prompt for the interpreter given an 'execute_reply' message.
364 """ Shows a prompt for the interpreter given an 'execute_reply' message.
365 """
365 """
366 self._show_interpreter_prompt()
366 self._show_interpreter_prompt()
367
367
368 #------ Signal handlers ----------------------------------------------------
368 #------ Signal handlers ----------------------------------------------------
369
369
370 def _document_contents_change(self, position, removed, added):
370 def _document_contents_change(self, position, removed, added):
371 """ Called whenever the document's content changes. Display a call tip
371 """ Called whenever the document's content changes. Display a call tip
372 if appropriate.
372 if appropriate.
373 """
373 """
374 # Calculate where the cursor should be *after* the change:
374 # Calculate where the cursor should be *after* the change:
375 position += added
375 position += added
376
376
377 document = self._control.document()
377 document = self._control.document()
378 if position == self._get_cursor().position():
378 if position == self._get_cursor().position():
379 self._call_tip()
379 self._call_tip()
@@ -1,299 +1,323 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_history_reply(self, msg):
80 """ Implemented to handle history replies, which are only supported by
81 the IPython kernel.
82 """
83 history_dict = msg['content']['history']
84 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
85 self._set_history(items)
86
87 def _handle_prompt_reply(self, msg):
88 """ Implemented to handle prompt number replies, which are only
89 supported by the IPython kernel.
90 """
91 content = msg['content']
92 self._show_interpreter_prompt(content['prompt_number'],
93 content['input_sep'])
94
79 def _handle_pyout(self, msg):
95 def _handle_pyout(self, msg):
80 """ Reimplemented for IPython-style "display hook".
96 """ Reimplemented for IPython-style "display hook".
81 """
97 """
82 if not self._hidden and self._is_from_this_session(msg):
98 if not self._hidden and self._is_from_this_session(msg):
83 content = msg['content']
99 content = msg['content']
84 prompt_number = content['prompt_number']
100 prompt_number = content['prompt_number']
85 self._append_plain_text(content['output_sep'])
101 self._append_plain_text(content['output_sep'])
86 self._append_html(self._make_out_prompt(prompt_number))
102 self._append_html(self._make_out_prompt(prompt_number))
87 self._append_plain_text(content['data'] + '\n' +
103 self._append_plain_text(content['data'] + '\n' +
88 content['output_sep2'])
104 content['output_sep2'])
89
105
106 def _started_channels(self):
107 """ Reimplemented to make a history request.
108 """
109 super(IPythonWidget, self)._started_channels()
110 # FIXME: Disabled until history requests are properly implemented.
111 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
112
90 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
91 # 'FrontendWidget' interface
114 # 'FrontendWidget' interface
92 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
93
116
94 def execute_file(self, path, hidden=False):
117 def execute_file(self, path, hidden=False):
95 """ Reimplemented to use the 'run' magic.
118 """ Reimplemented to use the 'run' magic.
96 """
119 """
97 self.execute('%%run %s' % path, hidden=hidden)
120 self.execute('%%run %s' % path, hidden=hidden)
98
121
99 #---------------------------------------------------------------------------
122 #---------------------------------------------------------------------------
100 # 'FrontendWidget' protected interface
123 # 'FrontendWidget' protected interface
101 #---------------------------------------------------------------------------
124 #---------------------------------------------------------------------------
102
125
103 def _get_banner(self):
126 def _get_banner(self):
104 """ Reimplemented to return IPython's default banner.
127 """ Reimplemented to return IPython's default banner.
105 """
128 """
106 return default_banner + '\n'
129 return default_banner + '\n'
107
130
108 def _process_execute_error(self, msg):
131 def _process_execute_error(self, msg):
109 """ Reimplemented for IPython-style traceback formatting.
132 """ Reimplemented for IPython-style traceback formatting.
110 """
133 """
111 content = msg['content']
134 content = msg['content']
112 traceback = '\n'.join(content['traceback']) + '\n'
135 traceback = '\n'.join(content['traceback']) + '\n'
113 if False:
136 if False:
114 # FIXME: For now, tracebacks come as plain text, so we can't use
137 # FIXME: For now, tracebacks come as plain text, so we can't use
115 # the html renderer yet. Once we refactor ultratb to produce
138 # the html renderer yet. Once we refactor ultratb to produce
116 # properly styled tracebacks, this branch should be the default
139 # properly styled tracebacks, this branch should be the default
117 traceback = traceback.replace(' ', '&nbsp;')
140 traceback = traceback.replace(' ', '&nbsp;')
118 traceback = traceback.replace('\n', '<br/>')
141 traceback = traceback.replace('\n', '<br/>')
119
142
120 ename = content['ename']
143 ename = content['ename']
121 ename_styled = '<span class="error">%s</span>' % ename
144 ename_styled = '<span class="error">%s</span>' % ename
122 traceback = traceback.replace(ename, ename_styled)
145 traceback = traceback.replace(ename, ename_styled)
123
146
124 self._append_html(traceback)
147 self._append_html(traceback)
125 else:
148 else:
126 # This is the fallback for now, using plain text with ansi escapes
149 # This is the fallback for now, using plain text with ansi escapes
127 self._append_plain_text(traceback)
150 self._append_plain_text(traceback)
128
151
129 def _process_execute_payload(self, item):
152 def _process_execute_payload(self, item):
130 """ Reimplemented to handle %edit and paging payloads.
153 """ Reimplemented to handle %edit and paging payloads.
131 """
154 """
132 if item['source'] == self._payload_source_edit:
155 if item['source'] == self._payload_source_edit:
133 self.edit(item['filename'], item['line_number'])
156 self.edit(item['filename'], item['line_number'])
134 return True
157 return True
135 elif item['source'] == self._payload_source_page:
158 elif item['source'] == self._payload_source_page:
136 self._page(item['data'])
159 self._page(item['data'])
137 return True
160 return True
138 else:
161 else:
139 return False
162 return False
140
163
141 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
164 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
142 """ Reimplemented for IPython-style prompts.
165 """ Reimplemented for IPython-style prompts.
143 """
166 """
144 # TODO: If a number was not specified, make a prompt number request.
167 # If a number was not specified, make a prompt number request.
145 if number is None:
168 if number is None:
146 number = 0
169 self.kernel_manager.xreq_channel.prompt()
170 return
147
171
148 # Show a new prompt and save information about it so that it can be
172 # Show a new prompt and save information about it so that it can be
149 # updated later if the prompt number turns out to be wrong.
173 # updated later if the prompt number turns out to be wrong.
150 self._append_plain_text(input_sep)
174 self._append_plain_text(input_sep)
151 self._show_prompt(self._make_in_prompt(number), html=True)
175 self._show_prompt(self._make_in_prompt(number), html=True)
152 block = self._control.document().lastBlock()
176 block = self._control.document().lastBlock()
153 length = len(self._prompt)
177 length = len(self._prompt)
154 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
178 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
155
179
156 # Update continuation prompt to reflect (possibly) new prompt length.
180 # Update continuation prompt to reflect (possibly) new prompt length.
157 self._set_continuation_prompt(
181 self._set_continuation_prompt(
158 self._make_continuation_prompt(self._prompt), html=True)
182 self._make_continuation_prompt(self._prompt), html=True)
159
183
160 def _show_interpreter_prompt_for_reply(self, msg):
184 def _show_interpreter_prompt_for_reply(self, msg):
161 """ Reimplemented for IPython-style prompts.
185 """ Reimplemented for IPython-style prompts.
162 """
186 """
163 # Update the old prompt number if necessary.
187 # Update the old prompt number if necessary.
164 content = msg['content']
188 content = msg['content']
165 previous_prompt_number = content['prompt_number']
189 previous_prompt_number = content['prompt_number']
166 if self._previous_prompt_obj and \
190 if self._previous_prompt_obj and \
167 self._previous_prompt_obj.number != previous_prompt_number:
191 self._previous_prompt_obj.number != previous_prompt_number:
168 block = self._previous_prompt_obj.block
192 block = self._previous_prompt_obj.block
169 if block.isValid():
193 if block.isValid():
170
194
171 # Remove the old prompt and insert a new prompt.
195 # Remove the old prompt and insert a new prompt.
172 cursor = QtGui.QTextCursor(block)
196 cursor = QtGui.QTextCursor(block)
173 cursor.movePosition(QtGui.QTextCursor.Right,
197 cursor.movePosition(QtGui.QTextCursor.Right,
174 QtGui.QTextCursor.KeepAnchor,
198 QtGui.QTextCursor.KeepAnchor,
175 self._previous_prompt_obj.length)
199 self._previous_prompt_obj.length)
176 prompt = self._make_in_prompt(previous_prompt_number)
200 prompt = self._make_in_prompt(previous_prompt_number)
177 self._prompt = self._insert_html_fetching_plain_text(
201 self._prompt = self._insert_html_fetching_plain_text(
178 cursor, prompt)
202 cursor, prompt)
179
203
180 # When the HTML is inserted, Qt blows away the syntax
204 # When the HTML is inserted, Qt blows away the syntax
181 # highlighting for the line, so we need to rehighlight it.
205 # highlighting for the line, so we need to rehighlight it.
182 self._highlighter.rehighlightBlock(cursor.block())
206 self._highlighter.rehighlightBlock(cursor.block())
183
207
184 self._previous_prompt_obj = None
208 self._previous_prompt_obj = None
185
209
186 # Show a new prompt with the kernel's estimated prompt number.
210 # Show a new prompt with the kernel's estimated prompt number.
187 next_prompt = content['next_prompt']
211 next_prompt = content['next_prompt']
188 self._show_interpreter_prompt(next_prompt['prompt_number'],
212 self._show_interpreter_prompt(next_prompt['prompt_number'],
189 next_prompt['input_sep'])
213 next_prompt['input_sep'])
190
214
191 #---------------------------------------------------------------------------
215 #---------------------------------------------------------------------------
192 # 'IPythonWidget' interface
216 # 'IPythonWidget' interface
193 #---------------------------------------------------------------------------
217 #---------------------------------------------------------------------------
194
218
195 def edit(self, filename, line=None):
219 def edit(self, filename, line=None):
196 """ Opens a Python script for editing.
220 """ Opens a Python script for editing.
197
221
198 Parameters:
222 Parameters:
199 -----------
223 -----------
200 filename : str
224 filename : str
201 A path to a local system file.
225 A path to a local system file.
202
226
203 line : int, optional
227 line : int, optional
204 A line of interest in the file.
228 A line of interest in the file.
205
229
206 Raises:
230 Raises:
207 -------
231 -------
208 OSError
232 OSError
209 If the editor command cannot be executed.
233 If the editor command cannot be executed.
210 """
234 """
211 if self._editor == 'default':
235 if self._editor == 'default':
212 url = QtCore.QUrl.fromLocalFile(filename)
236 url = QtCore.QUrl.fromLocalFile(filename)
213 if not QtGui.QDesktopServices.openUrl(url):
237 if not QtGui.QDesktopServices.openUrl(url):
214 message = 'Failed to open %s with the default application'
238 message = 'Failed to open %s with the default application'
215 raise OSError(message % repr(filename))
239 raise OSError(message % repr(filename))
216 elif self._editor is None:
240 elif self._editor is None:
217 self.custom_edit_requested.emit(filename, line)
241 self.custom_edit_requested.emit(filename, line)
218 else:
242 else:
219 Popen(self._editor + [filename])
243 Popen(self._editor + [filename])
220
244
221 def reset_styling(self):
245 def reset_styling(self):
222 """ Restores the default IPythonWidget styling.
246 """ Restores the default IPythonWidget styling.
223 """
247 """
224 self.set_styling(self.default_stylesheet, syntax_style='default')
248 self.set_styling(self.default_stylesheet, syntax_style='default')
225 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
249 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
226
250
227 def set_editor(self, editor):
251 def set_editor(self, editor):
228 """ Sets the editor to use with the %edit magic.
252 """ Sets the editor to use with the %edit magic.
229
253
230 Parameters:
254 Parameters:
231 -----------
255 -----------
232 editor : str or sequence of str
256 editor : str or sequence of str
233 A command suitable for use with Popen. This command will be executed
257 A command suitable for use with Popen. This command will be executed
234 with a single argument--a filename--when editing is requested.
258 with a single argument--a filename--when editing is requested.
235
259
236 This parameter also takes two special values:
260 This parameter also takes two special values:
237 'default' : Files will be edited with the system default
261 'default' : Files will be edited with the system default
238 application for Python files.
262 application for Python files.
239 'custom' : Emit a 'custom_edit_requested(str, int)' signal
263 'custom' : Emit a 'custom_edit_requested(str, int)' signal
240 instead of opening an editor.
264 instead of opening an editor.
241 """
265 """
242 if editor == 'default':
266 if editor == 'default':
243 self._editor = 'default'
267 self._editor = 'default'
244 elif editor == 'custom':
268 elif editor == 'custom':
245 self._editor = None
269 self._editor = None
246 elif isinstance(editor, basestring):
270 elif isinstance(editor, basestring):
247 self._editor = [ editor ]
271 self._editor = [ editor ]
248 else:
272 else:
249 self._editor = list(editor)
273 self._editor = list(editor)
250
274
251 def set_styling(self, stylesheet, syntax_style=None):
275 def set_styling(self, stylesheet, syntax_style=None):
252 """ Sets the IPythonWidget styling.
276 """ Sets the IPythonWidget styling.
253
277
254 Parameters:
278 Parameters:
255 -----------
279 -----------
256 stylesheet : str
280 stylesheet : str
257 A CSS stylesheet. The stylesheet can contain classes for:
281 A CSS stylesheet. The stylesheet can contain classes for:
258 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
282 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
259 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
283 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
260 3. IPython: .error, .in-prompt, .out-prompt, etc.
284 3. IPython: .error, .in-prompt, .out-prompt, etc.
261
285
262 syntax_style : str or None [default None]
286 syntax_style : str or None [default None]
263 If specified, use the Pygments style with given name. Otherwise,
287 If specified, use the Pygments style with given name. Otherwise,
264 the stylesheet is queried for Pygments style information.
288 the stylesheet is queried for Pygments style information.
265 """
289 """
266 self.setStyleSheet(stylesheet)
290 self.setStyleSheet(stylesheet)
267 self._control.document().setDefaultStyleSheet(stylesheet)
291 self._control.document().setDefaultStyleSheet(stylesheet)
268 if self._page_control:
292 if self._page_control:
269 self._page_control.document().setDefaultStyleSheet(stylesheet)
293 self._page_control.document().setDefaultStyleSheet(stylesheet)
270
294
271 if syntax_style is None:
295 if syntax_style is None:
272 self._highlighter.set_style_sheet(stylesheet)
296 self._highlighter.set_style_sheet(stylesheet)
273 else:
297 else:
274 self._highlighter.set_style(syntax_style)
298 self._highlighter.set_style(syntax_style)
275
299
276 #---------------------------------------------------------------------------
300 #---------------------------------------------------------------------------
277 # 'IPythonWidget' protected interface
301 # 'IPythonWidget' protected interface
278 #---------------------------------------------------------------------------
302 #---------------------------------------------------------------------------
279
303
280 def _make_in_prompt(self, number):
304 def _make_in_prompt(self, number):
281 """ Given a prompt number, returns an HTML In prompt.
305 """ Given a prompt number, returns an HTML In prompt.
282 """
306 """
283 body = self.in_prompt % number
307 body = self.in_prompt % number
284 return '<span class="in-prompt">%s</span>' % body
308 return '<span class="in-prompt">%s</span>' % body
285
309
286 def _make_continuation_prompt(self, prompt):
310 def _make_continuation_prompt(self, prompt):
287 """ Given a plain text version of an In prompt, returns an HTML
311 """ Given a plain text version of an In prompt, returns an HTML
288 continuation prompt.
312 continuation prompt.
289 """
313 """
290 end_chars = '...: '
314 end_chars = '...: '
291 space_count = len(prompt.lstrip('\n')) - len(end_chars)
315 space_count = len(prompt.lstrip('\n')) - len(end_chars)
292 body = '&nbsp;' * space_count + end_chars
316 body = '&nbsp;' * space_count + end_chars
293 return '<span class="in-prompt">%s</span>' % body
317 return '<span class="in-prompt">%s</span>' % body
294
318
295 def _make_out_prompt(self, number):
319 def _make_out_prompt(self, number):
296 """ Given a prompt number, returns an HTML Out prompt.
320 """ Given a prompt number, returns an HTML Out prompt.
297 """
321 """
298 body = self.out_prompt % number
322 body = self.out_prompt % number
299 return '<span class="out-prompt">%s</span>' % body
323 return '<span class="out-prompt">%s</span>' % body
@@ -1,398 +1,399 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """A simple interactive kernel that talks to a frontend over 0MQ.
2 """A simple interactive kernel that talks to a frontend over 0MQ.
3
3
4 Things to do:
4 Things to do:
5
5
6 * Implement `set_parent` logic. Right before doing exec, the Kernel should
6 * Implement `set_parent` logic. Right before doing exec, the Kernel should
7 call set_parent on all the PUB objects with the message about to be executed.
7 call set_parent on all the PUB objects with the message about to be executed.
8 * Implement random port and security key logic.
8 * Implement random port and security key logic.
9 * Implement control messages.
9 * Implement control messages.
10 * Implement event loop and poll version.
10 * Implement event loop and poll version.
11 """
11 """
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 # Standard library imports.
17 # Standard library imports.
18 import __builtin__
18 import __builtin__
19 import sys
19 import sys
20 import time
20 import time
21 import traceback
21 import traceback
22
22
23 # System library imports.
23 # System library imports.
24 import zmq
24 import zmq
25
25
26 # Local imports.
26 # Local imports.
27 from IPython.config.configurable import Configurable
27 from IPython.config.configurable import Configurable
28 from IPython.utils.traitlets import Instance
28 from IPython.utils.traitlets import Instance
29 from completer import KernelCompleter
29 from completer import KernelCompleter
30 from entry_point import base_launch_kernel, make_argument_parser, make_kernel, \
30 from entry_point import base_launch_kernel, make_argument_parser, make_kernel, \
31 start_kernel
31 start_kernel
32 from iostream import OutStream
32 from iostream import OutStream
33 from session import Session, Message
33 from session import Session, Message
34 from zmqshell import ZMQInteractiveShell
34 from zmqshell import ZMQInteractiveShell
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Main kernel class
37 # Main kernel class
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39
39
40 class Kernel(Configurable):
40 class Kernel(Configurable):
41
41
42 #---------------------------------------------------------------------------
42 #---------------------------------------------------------------------------
43 # Kernel interface
43 # Kernel interface
44 #---------------------------------------------------------------------------
44 #---------------------------------------------------------------------------
45
45
46 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
46 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
47 session = Instance(Session)
47 session = Instance(Session)
48 reply_socket = Instance('zmq.Socket')
48 reply_socket = Instance('zmq.Socket')
49 pub_socket = Instance('zmq.Socket')
49 pub_socket = Instance('zmq.Socket')
50 req_socket = Instance('zmq.Socket')
50 req_socket = Instance('zmq.Socket')
51
51
52 # Maps user-friendly backend names to matplotlib backend identifiers.
52 # Maps user-friendly backend names to matplotlib backend identifiers.
53 _pylab_map = { 'tk': 'TkAgg',
53 _pylab_map = { 'tk': 'TkAgg',
54 'gtk': 'GTKAgg',
54 'gtk': 'GTKAgg',
55 'wx': 'WXAgg',
55 'wx': 'WXAgg',
56 'qt': 'Qt4Agg', # qt3 not supported
56 'qt': 'Qt4Agg', # qt3 not supported
57 'qt4': 'Qt4Agg',
57 'qt4': 'Qt4Agg',
58 'payload-svg' : \
58 'payload-svg' : \
59 'module://IPython.zmq.pylab.backend_payload_svg' }
59 'module://IPython.zmq.pylab.backend_payload_svg' }
60
60
61 def __init__(self, **kwargs):
61 def __init__(self, **kwargs):
62 super(Kernel, self).__init__(**kwargs)
62 super(Kernel, self).__init__(**kwargs)
63
63
64 # Initialize the InteractiveShell subclass
64 # Initialize the InteractiveShell subclass
65 self.shell = ZMQInteractiveShell.instance()
65 self.shell = ZMQInteractiveShell.instance()
66 self.shell.displayhook.session = self.session
66 self.shell.displayhook.session = self.session
67 self.shell.displayhook.pub_socket = self.pub_socket
67 self.shell.displayhook.pub_socket = self.pub_socket
68
68
69 # TMP - hack while developing
69 # TMP - hack while developing
70 self.shell._reply_content = None
70 self.shell._reply_content = None
71
71
72 # Build dict of handlers for message types
72 # Build dict of handlers for message types
73 msg_types = [ 'execute_request', 'complete_request',
73 msg_types = [ 'execute_request', 'complete_request',
74 'object_info_request', 'prompt_request',
74 'object_info_request', 'prompt_request',
75 'history_request' ]
75 'history_request' ]
76 self.handlers = {}
76 self.handlers = {}
77 for msg_type in msg_types:
77 for msg_type in msg_types:
78 self.handlers[msg_type] = getattr(self, msg_type)
78 self.handlers[msg_type] = getattr(self, msg_type)
79
79
80 def activate_pylab(self, backend=None, import_all=True):
80 def activate_pylab(self, backend=None, import_all=True):
81 """ Activates pylab in this kernel's namespace.
81 """ Activates pylab in this kernel's namespace.
82
82
83 Parameters:
83 Parameters:
84 -----------
84 -----------
85 backend : str, optional
85 backend : str, optional
86 A valid backend name.
86 A valid backend name.
87
87
88 import_all : bool, optional
88 import_all : bool, optional
89 If true, an 'import *' is done from numpy and pylab.
89 If true, an 'import *' is done from numpy and pylab.
90 """
90 """
91 # FIXME: This is adapted from IPython.lib.pylabtools.pylab_activate.
91 # FIXME: This is adapted from IPython.lib.pylabtools.pylab_activate.
92 # Common functionality should be refactored.
92 # Common functionality should be refactored.
93
93
94 # We must set the desired backend before importing pylab.
94 # We must set the desired backend before importing pylab.
95 import matplotlib
95 import matplotlib
96 if backend:
96 if backend:
97 backend_id = self._pylab_map[backend]
97 backend_id = self._pylab_map[backend]
98 if backend_id.startswith('module://'):
98 if backend_id.startswith('module://'):
99 # Work around bug in matplotlib: matplotlib.use converts the
99 # Work around bug in matplotlib: matplotlib.use converts the
100 # backend_id to lowercase even if a module name is specified!
100 # backend_id to lowercase even if a module name is specified!
101 matplotlib.rcParams['backend'] = backend_id
101 matplotlib.rcParams['backend'] = backend_id
102 else:
102 else:
103 matplotlib.use(backend_id)
103 matplotlib.use(backend_id)
104
104
105 # Import numpy as np/pyplot as plt are conventions we're trying to
105 # Import numpy as np/pyplot as plt are conventions we're trying to
106 # somewhat standardize on. Making them available to users by default
106 # somewhat standardize on. Making them available to users by default
107 # will greatly help this.
107 # will greatly help this.
108 exec ("import numpy\n"
108 exec ("import numpy\n"
109 "import matplotlib\n"
109 "import matplotlib\n"
110 "from matplotlib import pylab, mlab, pyplot\n"
110 "from matplotlib import pylab, mlab, pyplot\n"
111 "np = numpy\n"
111 "np = numpy\n"
112 "plt = pyplot\n"
112 "plt = pyplot\n"
113 ) in self.shell.user_ns
113 ) in self.shell.user_ns
114
114
115 if import_all:
115 if import_all:
116 exec("from matplotlib.pylab import *\n"
116 exec("from matplotlib.pylab import *\n"
117 "from numpy import *\n") in self.shell.user_ns
117 "from numpy import *\n") in self.shell.user_ns
118
118
119 matplotlib.interactive(True)
119 matplotlib.interactive(True)
120
120
121 def start(self):
121 def start(self):
122 """ Start the kernel main loop.
122 """ Start the kernel main loop.
123 """
123 """
124 while True:
124 while True:
125 ident = self.reply_socket.recv()
125 ident = self.reply_socket.recv()
126 assert self.reply_socket.rcvmore(), "Missing message part."
126 assert self.reply_socket.rcvmore(), "Missing message part."
127 msg = self.reply_socket.recv_json()
127 msg = self.reply_socket.recv_json()
128 omsg = Message(msg)
128 omsg = Message(msg)
129 print>>sys.__stdout__
129 print>>sys.__stdout__
130 print>>sys.__stdout__, omsg
130 print>>sys.__stdout__, omsg
131 handler = self.handlers.get(omsg.msg_type, None)
131 handler = self.handlers.get(omsg.msg_type, None)
132 if handler is None:
132 if handler is None:
133 print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg
133 print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg
134 else:
134 else:
135 handler(ident, omsg)
135 handler(ident, omsg)
136
136
137 #---------------------------------------------------------------------------
137 #---------------------------------------------------------------------------
138 # Kernel request handlers
138 # Kernel request handlers
139 #---------------------------------------------------------------------------
139 #---------------------------------------------------------------------------
140
140
141 def execute_request(self, ident, parent):
141 def execute_request(self, ident, parent):
142 try:
142 try:
143 code = parent[u'content'][u'code']
143 code = parent[u'content'][u'code']
144 except:
144 except:
145 print>>sys.__stderr__, "Got bad msg: "
145 print>>sys.__stderr__, "Got bad msg: "
146 print>>sys.__stderr__, Message(parent)
146 print>>sys.__stderr__, Message(parent)
147 return
147 return
148 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
148 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
149 self.pub_socket.send_json(pyin_msg)
149 self.pub_socket.send_json(pyin_msg)
150
150
151 try:
151 try:
152 # Replace raw_input. Note that is not sufficient to replace
152 # Replace raw_input. Note that is not sufficient to replace
153 # raw_input in the user namespace.
153 # raw_input in the user namespace.
154 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
154 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
155 __builtin__.raw_input = raw_input
155 __builtin__.raw_input = raw_input
156
156
157 # Set the parent message of the display hook and out streams.
157 # Set the parent message of the display hook and out streams.
158 self.shell.displayhook.set_parent(parent)
158 self.shell.displayhook.set_parent(parent)
159 sys.stdout.set_parent(parent)
159 sys.stdout.set_parent(parent)
160 sys.stderr.set_parent(parent)
160 sys.stderr.set_parent(parent)
161
161
162 # FIXME: runlines calls the exception handler itself. We should
162 # FIXME: runlines calls the exception handler itself. We should
163 # clean this up.
163 # clean this up.
164 self.shell._reply_content = None
164 self.shell._reply_content = None
165 self.shell.runlines(code)
165 self.shell.runlines(code)
166 except:
166 except:
167 # FIXME: this code right now isn't being used yet by default,
167 # FIXME: this code right now isn't being used yet by default,
168 # because the runlines() call above directly fires off exception
168 # because the runlines() call above directly fires off exception
169 # reporting. This code, therefore, is only active in the scenario
169 # reporting. This code, therefore, is only active in the scenario
170 # where runlines itself has an unhandled exception. We need to
170 # where runlines itself has an unhandled exception. We need to
171 # uniformize this, for all exception construction to come from a
171 # uniformize this, for all exception construction to come from a
172 # single location in the codbase.
172 # single location in the codbase.
173 etype, evalue, tb = sys.exc_info()
173 etype, evalue, tb = sys.exc_info()
174 tb_list = traceback.format_exception(etype, evalue, tb)
174 tb_list = traceback.format_exception(etype, evalue, tb)
175 reply_content = self.shell._showtraceback(etype, evalue, tb_list)
175 reply_content = self.shell._showtraceback(etype, evalue, tb_list)
176 else:
176 else:
177 payload = self.shell.payload_manager.read_payload()
177 payload = self.shell.payload_manager.read_payload()
178 # Be agressive about clearing the payload because we don't want
178 # Be agressive about clearing the payload because we don't want
179 # it to sit in memory until the next execute_request comes in.
179 # it to sit in memory until the next execute_request comes in.
180 self.shell.payload_manager.clear_payload()
180 self.shell.payload_manager.clear_payload()
181 reply_content = { 'status' : 'ok', 'payload' : payload }
181 reply_content = { 'status' : 'ok', 'payload' : payload }
182
182
183 # Compute the prompt information
183 # Compute the prompt information
184 prompt_number = self.shell.displayhook.prompt_count
184 prompt_number = self.shell.displayhook.prompt_count
185 reply_content['prompt_number'] = prompt_number
185 reply_content['prompt_number'] = prompt_number
186 prompt_string = self.shell.displayhook.prompt1.peek_next_prompt()
186 prompt_string = self.shell.displayhook.prompt1.peek_next_prompt()
187 next_prompt = {'prompt_string' : prompt_string,
187 next_prompt = {'prompt_string' : prompt_string,
188 'prompt_number' : prompt_number+1,
188 'prompt_number' : prompt_number+1,
189 'input_sep' : self.shell.displayhook.input_sep}
189 'input_sep' : self.shell.displayhook.input_sep}
190 reply_content['next_prompt'] = next_prompt
190 reply_content['next_prompt'] = next_prompt
191
191
192 # TMP - fish exception info out of shell, possibly left there by
192 # TMP - fish exception info out of shell, possibly left there by
193 # runlines
193 # runlines
194 if self.shell._reply_content is not None:
194 if self.shell._reply_content is not None:
195 reply_content.update(self.shell._reply_content)
195 reply_content.update(self.shell._reply_content)
196
196
197 # Flush output before sending the reply.
197 # Flush output before sending the reply.
198 sys.stderr.flush()
198 sys.stderr.flush()
199 sys.stdout.flush()
199 sys.stdout.flush()
200
200
201 # Send the reply.
201 # Send the reply.
202 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
202 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
203 print>>sys.__stdout__, Message(reply_msg)
203 print>>sys.__stdout__, Message(reply_msg)
204 self.reply_socket.send(ident, zmq.SNDMORE)
204 self.reply_socket.send(ident, zmq.SNDMORE)
205 self.reply_socket.send_json(reply_msg)
205 self.reply_socket.send_json(reply_msg)
206 if reply_msg['content']['status'] == u'error':
206 if reply_msg['content']['status'] == u'error':
207 self._abort_queue()
207 self._abort_queue()
208
208
209 def complete_request(self, ident, parent):
209 def complete_request(self, ident, parent):
210 matches = {'matches' : self._complete(parent),
210 matches = {'matches' : self._complete(parent),
211 'status' : 'ok'}
211 'status' : 'ok'}
212 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
212 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
213 matches, parent, ident)
213 matches, parent, ident)
214 print >> sys.__stdout__, completion_msg
214 print >> sys.__stdout__, completion_msg
215
215
216 def object_info_request(self, ident, parent):
216 def object_info_request(self, ident, parent):
217 context = parent['content']['oname'].split('.')
217 context = parent['content']['oname'].split('.')
218 object_info = self._object_info(context)
218 object_info = self._object_info(context)
219 msg = self.session.send(self.reply_socket, 'object_info_reply',
219 msg = self.session.send(self.reply_socket, 'object_info_reply',
220 object_info, parent, ident)
220 object_info, parent, ident)
221 print >> sys.__stdout__, msg
221 print >> sys.__stdout__, msg
222
222
223 def prompt_request(self, ident, parent):
223 def prompt_request(self, ident, parent):
224 prompt_number = self.shell.displayhook.prompt_count
224 prompt_number = self.shell.displayhook.prompt_count
225 prompt_string = self.shell.displayhook.prompt1.peek_next_prompt()
225 prompt_string = self.shell.displayhook.prompt1.peek_next_prompt()
226 content = {'prompt_string' : prompt_string,
226 content = {'prompt_string' : prompt_string,
227 'prompt_number' : prompt_number+1}
227 'prompt_number' : prompt_number+1,
228 'input_sep' : self.shell.displayhook.input_sep}
228 msg = self.session.send(self.reply_socket, 'prompt_reply',
229 msg = self.session.send(self.reply_socket, 'prompt_reply',
229 content, parent, ident)
230 content, parent, ident)
230 print >> sys.__stdout__, msg
231 print >> sys.__stdout__, msg
231
232
232 def history_request(self, ident, parent):
233 def history_request(self, ident, parent):
233 output = parent['content'].get('output', True)
234 output = parent['content']['output']
234 index = parent['content'].get('index')
235 index = parent['content']['index']
235 raw = parent['content'].get('raw', False)
236 raw = parent['content']['raw']
236 hist = self.shell.get_history(index=index, raw=raw, output=output)
237 hist = self.shell.get_history(index=index, raw=raw, output=output)
237 content = {'history' : hist}
238 content = {'history' : hist}
238 msg = self.session.send(self.reply_socket, 'history_reply',
239 msg = self.session.send(self.reply_socket, 'history_reply',
239 content, parent, ident)
240 content, parent, ident)
240 print >> sys.__stdout__, msg
241 print >> sys.__stdout__, msg
241
242
242 #---------------------------------------------------------------------------
243 #---------------------------------------------------------------------------
243 # Protected interface
244 # Protected interface
244 #---------------------------------------------------------------------------
245 #---------------------------------------------------------------------------
245
246
246 def _abort_queue(self):
247 def _abort_queue(self):
247 while True:
248 while True:
248 try:
249 try:
249 ident = self.reply_socket.recv(zmq.NOBLOCK)
250 ident = self.reply_socket.recv(zmq.NOBLOCK)
250 except zmq.ZMQError, e:
251 except zmq.ZMQError, e:
251 if e.errno == zmq.EAGAIN:
252 if e.errno == zmq.EAGAIN:
252 break
253 break
253 else:
254 else:
254 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
255 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
255 msg = self.reply_socket.recv_json()
256 msg = self.reply_socket.recv_json()
256 print>>sys.__stdout__, "Aborting:"
257 print>>sys.__stdout__, "Aborting:"
257 print>>sys.__stdout__, Message(msg)
258 print>>sys.__stdout__, Message(msg)
258 msg_type = msg['msg_type']
259 msg_type = msg['msg_type']
259 reply_type = msg_type.split('_')[0] + '_reply'
260 reply_type = msg_type.split('_')[0] + '_reply'
260 reply_msg = self.session.msg(reply_type, {'status' : 'aborted'}, msg)
261 reply_msg = self.session.msg(reply_type, {'status' : 'aborted'}, msg)
261 print>>sys.__stdout__, Message(reply_msg)
262 print>>sys.__stdout__, Message(reply_msg)
262 self.reply_socket.send(ident,zmq.SNDMORE)
263 self.reply_socket.send(ident,zmq.SNDMORE)
263 self.reply_socket.send_json(reply_msg)
264 self.reply_socket.send_json(reply_msg)
264 # We need to wait a bit for requests to come in. This can probably
265 # We need to wait a bit for requests to come in. This can probably
265 # be set shorter for true asynchronous clients.
266 # be set shorter for true asynchronous clients.
266 time.sleep(0.1)
267 time.sleep(0.1)
267
268
268 def _raw_input(self, prompt, ident, parent):
269 def _raw_input(self, prompt, ident, parent):
269 # Flush output before making the request.
270 # Flush output before making the request.
270 sys.stderr.flush()
271 sys.stderr.flush()
271 sys.stdout.flush()
272 sys.stdout.flush()
272
273
273 # Send the input request.
274 # Send the input request.
274 content = dict(prompt=prompt)
275 content = dict(prompt=prompt)
275 msg = self.session.msg(u'input_request', content, parent)
276 msg = self.session.msg(u'input_request', content, parent)
276 self.req_socket.send_json(msg)
277 self.req_socket.send_json(msg)
277
278
278 # Await a response.
279 # Await a response.
279 reply = self.req_socket.recv_json()
280 reply = self.req_socket.recv_json()
280 try:
281 try:
281 value = reply['content']['value']
282 value = reply['content']['value']
282 except:
283 except:
283 print>>sys.__stderr__, "Got bad raw_input reply: "
284 print>>sys.__stderr__, "Got bad raw_input reply: "
284 print>>sys.__stderr__, Message(parent)
285 print>>sys.__stderr__, Message(parent)
285 value = ''
286 value = ''
286 return value
287 return value
287
288
288 def _complete(self, msg):
289 def _complete(self, msg):
289 #from IPython.utils.io import rprint # dbg
290 #from IPython.utils.io import rprint # dbg
290 #rprint('\n\n**MSG**\n\n', msg) # dbg
291 #rprint('\n\n**MSG**\n\n', msg) # dbg
291 #import traceback; rprint(''.join(traceback.format_stack())) # dbg
292 #import traceback; rprint(''.join(traceback.format_stack())) # dbg
292 c = msg['content']
293 c = msg['content']
293 try:
294 try:
294 cpos = int(c['cursor_pos'])
295 cpos = int(c['cursor_pos'])
295 except:
296 except:
296 # If we don't get something that we can convert to an integer, at
297 # If we don't get something that we can convert to an integer, at
297 # leasat attempt the completion guessing the cursor is at the end
298 # leasat attempt the completion guessing the cursor is at the end
298 # of the text
299 # of the text
299 cpos = len(c['text'])
300 cpos = len(c['text'])
300 return self.shell.complete(c['text'], c['line'], cpos)
301 return self.shell.complete(c['text'], c['line'], cpos)
301
302
302 def _object_info(self, context):
303 def _object_info(self, context):
303 symbol, leftover = self._symbol_from_context(context)
304 symbol, leftover = self._symbol_from_context(context)
304 if symbol is not None and not leftover:
305 if symbol is not None and not leftover:
305 doc = getattr(symbol, '__doc__', '')
306 doc = getattr(symbol, '__doc__', '')
306 else:
307 else:
307 doc = ''
308 doc = ''
308 object_info = dict(docstring = doc)
309 object_info = dict(docstring = doc)
309 return object_info
310 return object_info
310
311
311 def _symbol_from_context(self, context):
312 def _symbol_from_context(self, context):
312 if not context:
313 if not context:
313 return None, context
314 return None, context
314
315
315 base_symbol_string = context[0]
316 base_symbol_string = context[0]
316 symbol = self.shell.user_ns.get(base_symbol_string, None)
317 symbol = self.shell.user_ns.get(base_symbol_string, None)
317 if symbol is None:
318 if symbol is None:
318 symbol = __builtin__.__dict__.get(base_symbol_string, None)
319 symbol = __builtin__.__dict__.get(base_symbol_string, None)
319 if symbol is None:
320 if symbol is None:
320 return None, context
321 return None, context
321
322
322 context = context[1:]
323 context = context[1:]
323 for i, name in enumerate(context):
324 for i, name in enumerate(context):
324 new_symbol = getattr(symbol, name, None)
325 new_symbol = getattr(symbol, name, None)
325 if new_symbol is None:
326 if new_symbol is None:
326 return symbol, context[i:]
327 return symbol, context[i:]
327 else:
328 else:
328 symbol = new_symbol
329 symbol = new_symbol
329
330
330 return symbol, []
331 return symbol, []
331
332
332 #-----------------------------------------------------------------------------
333 #-----------------------------------------------------------------------------
333 # Kernel main and launch functions
334 # Kernel main and launch functions
334 #-----------------------------------------------------------------------------
335 #-----------------------------------------------------------------------------
335
336
336 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, independent=False,
337 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, independent=False,
337 pylab=False):
338 pylab=False):
338 """ Launches a localhost kernel, binding to the specified ports.
339 """ Launches a localhost kernel, binding to the specified ports.
339
340
340 Parameters
341 Parameters
341 ----------
342 ----------
342 xrep_port : int, optional
343 xrep_port : int, optional
343 The port to use for XREP channel.
344 The port to use for XREP channel.
344
345
345 pub_port : int, optional
346 pub_port : int, optional
346 The port to use for the SUB channel.
347 The port to use for the SUB channel.
347
348
348 req_port : int, optional
349 req_port : int, optional
349 The port to use for the REQ (raw input) channel.
350 The port to use for the REQ (raw input) channel.
350
351
351 independent : bool, optional (default False)
352 independent : bool, optional (default False)
352 If set, the kernel process is guaranteed to survive if this process
353 If set, the kernel process is guaranteed to survive if this process
353 dies. If not set, an effort is made to ensure that the kernel is killed
354 dies. If not set, an effort is made to ensure that the kernel is killed
354 when this process dies. Note that in this case it is still good practice
355 when this process dies. Note that in this case it is still good practice
355 to kill kernels manually before exiting.
356 to kill kernels manually before exiting.
356
357
357 pylab : bool or string, optional (default False)
358 pylab : bool or string, optional (default False)
358 If not False, the kernel will be launched with pylab enabled. If a
359 If not False, the kernel will be launched with pylab enabled. If a
359 string is passed, matplotlib will use the specified backend. Otherwise,
360 string is passed, matplotlib will use the specified backend. Otherwise,
360 matplotlib's default backend will be used.
361 matplotlib's default backend will be used.
361
362
362 Returns
363 Returns
363 -------
364 -------
364 A tuple of form:
365 A tuple of form:
365 (kernel_process, xrep_port, pub_port, req_port)
366 (kernel_process, xrep_port, pub_port, req_port)
366 where kernel_process is a Popen object and the ports are integers.
367 where kernel_process is a Popen object and the ports are integers.
367 """
368 """
368 extra_arguments = []
369 extra_arguments = []
369 if pylab:
370 if pylab:
370 extra_arguments.append('--pylab')
371 extra_arguments.append('--pylab')
371 if isinstance(pylab, basestring):
372 if isinstance(pylab, basestring):
372 extra_arguments.append(pylab)
373 extra_arguments.append(pylab)
373 return base_launch_kernel('from IPython.zmq.ipkernel import main; main()',
374 return base_launch_kernel('from IPython.zmq.ipkernel import main; main()',
374 xrep_port, pub_port, req_port, independent,
375 xrep_port, pub_port, req_port, independent,
375 extra_arguments)
376 extra_arguments)
376
377
377 def main():
378 def main():
378 """ The IPython kernel main entry point.
379 """ The IPython kernel main entry point.
379 """
380 """
380 parser = make_argument_parser()
381 parser = make_argument_parser()
381 parser.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
382 parser.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
382 const='auto', help = \
383 const='auto', help = \
383 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
384 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
384 given, the GUI backend is matplotlib's, otherwise use one of: \
385 given, the GUI backend is matplotlib's, otherwise use one of: \
385 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
386 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
386 namespace = parser.parse_args()
387 namespace = parser.parse_args()
387
388
388 kernel = make_kernel(namespace, Kernel, OutStream)
389 kernel = make_kernel(namespace, Kernel, OutStream)
389 if namespace.pylab:
390 if namespace.pylab:
390 if namespace.pylab == 'auto':
391 if namespace.pylab == 'auto':
391 kernel.activate_pylab()
392 kernel.activate_pylab()
392 else:
393 else:
393 kernel.activate_pylab(namespace.pylab)
394 kernel.activate_pylab(namespace.pylab)
394
395
395 start_kernel(namespace, kernel)
396 start_kernel(namespace, kernel)
396
397
397 if __name__ == '__main__':
398 if __name__ == '__main__':
398 main()
399 main()
@@ -1,581 +1,617 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, silent=False):
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 silent : bool, optional (default False)
174 If set, the kernel will execute the code as quietly possible.
173
175
174 Returns
176 Returns
175 -------
177 -------
176 The msg_id of the message sent.
178 The msg_id of the message sent.
177 """
179 """
178 # Create class for content/msg creation. Related to, but possibly
180 # Create class for content/msg creation. Related to, but possibly
179 # not in Session.
181 # not in Session.
180 content = dict(code=code)
182 content = dict(code=code, silent=silent)
181 msg = self.session.msg('execute_request', content)
183 msg = self.session.msg('execute_request', content)
182 self._queue_request(msg)
184 self._queue_request(msg)
183 return msg['header']['msg_id']
185 return msg['header']['msg_id']
184
186
185 def complete(self, text, line, cursor_pos, block=None):
187 def complete(self, text, line, cursor_pos, block=None):
186 """Tab complete text in the kernel's namespace.
188 """Tab complete text in the kernel's namespace.
187
189
188 Parameters
190 Parameters
189 ----------
191 ----------
190 text : str
192 text : str
191 The text to complete.
193 The text to complete.
192 line : str
194 line : str
193 The full line of text that is the surrounding context for the
195 The full line of text that is the surrounding context for the
194 text to complete.
196 text to complete.
195 cursor_pos : int
197 cursor_pos : int
196 The position of the cursor in the line where the completion was
198 The position of the cursor in the line where the completion was
197 requested.
199 requested.
198 block : str, optional
200 block : str, optional
199 The full block of code in which the completion is being requested.
201 The full block of code in which the completion is being requested.
200
202
201 Returns
203 Returns
202 -------
204 -------
203 The msg_id of the message sent.
205 The msg_id of the message sent.
204 """
206 """
205 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
207 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
206 msg = self.session.msg('complete_request', content)
208 msg = self.session.msg('complete_request', content)
207 self._queue_request(msg)
209 self._queue_request(msg)
208 return msg['header']['msg_id']
210 return msg['header']['msg_id']
209
211
210 def object_info(self, oname):
212 def object_info(self, oname):
211 """Get metadata information about an object.
213 """Get metadata information about an object.
212
214
213 Parameters
215 Parameters
214 ----------
216 ----------
215 oname : str
217 oname : str
216 A string specifying the object name.
218 A string specifying the object name.
217
219
218 Returns
220 Returns
219 -------
221 -------
220 The msg_id of the message sent.
222 The msg_id of the message sent.
221 """
223 """
222 content = dict(oname=oname)
224 content = dict(oname=oname)
223 msg = self.session.msg('object_info_request', content)
225 msg = self.session.msg('object_info_request', content)
224 self._queue_request(msg)
226 self._queue_request(msg)
225 return msg['header']['msg_id']
227 return msg['header']['msg_id']
226
228
229 def history(self, index=None, raw=False, output=True):
230 """Get the history list.
231
232 Parameters
233 ----------
234 index : n or (n1, n2) or None
235 If n, then the last entries. If a tuple, then all in
236 range(n1, n2). If None, then all entries. Raises IndexError if
237 the format of index is incorrect.
238 raw : bool
239 If True, return the raw input.
240 output : bool
241 If True, then return the output as well.
242
243 Returns
244 -------
245 The msg_id of the message sent.
246 """
247 content = dict(index=index, raw=raw, output=output)
248 msg = self.session.msg('history_request', content)
249 self._queue_request(msg)
250 return msg['header']['msg_id']
251
252 def prompt(self):
253 """Requests a prompt number from the kernel.
254
255 Returns
256 -------
257 The msg_id of the message sent.
258 """
259 msg = self.session.msg('prompt_request')
260 self._queue_request(msg)
261 return msg['header']['msg_id']
262
227 def _handle_events(self, socket, events):
263 def _handle_events(self, socket, events):
228 if events & POLLERR:
264 if events & POLLERR:
229 self._handle_err()
265 self._handle_err()
230 if events & POLLOUT:
266 if events & POLLOUT:
231 self._handle_send()
267 self._handle_send()
232 if events & POLLIN:
268 if events & POLLIN:
233 self._handle_recv()
269 self._handle_recv()
234
270
235 def _handle_recv(self):
271 def _handle_recv(self):
236 msg = self.socket.recv_json()
272 msg = self.socket.recv_json()
237 self.call_handlers(msg)
273 self.call_handlers(msg)
238
274
239 def _handle_send(self):
275 def _handle_send(self):
240 try:
276 try:
241 msg = self.command_queue.get(False)
277 msg = self.command_queue.get(False)
242 except Empty:
278 except Empty:
243 pass
279 pass
244 else:
280 else:
245 self.socket.send_json(msg)
281 self.socket.send_json(msg)
246 if self.command_queue.empty():
282 if self.command_queue.empty():
247 self.drop_io_state(POLLOUT)
283 self.drop_io_state(POLLOUT)
248
284
249 def _handle_err(self):
285 def _handle_err(self):
250 # We don't want to let this go silently, so eventually we should log.
286 # We don't want to let this go silently, so eventually we should log.
251 raise zmq.ZMQError()
287 raise zmq.ZMQError()
252
288
253 def _queue_request(self, msg):
289 def _queue_request(self, msg):
254 self.command_queue.put(msg)
290 self.command_queue.put(msg)
255 self.add_io_state(POLLOUT)
291 self.add_io_state(POLLOUT)
256
292
257
293
258 class SubSocketChannel(ZmqSocketChannel):
294 class SubSocketChannel(ZmqSocketChannel):
259 """The SUB channel which listens for messages that the kernel publishes.
295 """The SUB channel which listens for messages that the kernel publishes.
260 """
296 """
261
297
262 def __init__(self, context, session, address):
298 def __init__(self, context, session, address):
263 super(SubSocketChannel, self).__init__(context, session, address)
299 super(SubSocketChannel, self).__init__(context, session, address)
264
300
265 def run(self):
301 def run(self):
266 """The thread's main activity. Call start() instead."""
302 """The thread's main activity. Call start() instead."""
267 self.socket = self.context.socket(zmq.SUB)
303 self.socket = self.context.socket(zmq.SUB)
268 self.socket.setsockopt(zmq.SUBSCRIBE,'')
304 self.socket.setsockopt(zmq.SUBSCRIBE,'')
269 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
305 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
270 self.socket.connect('tcp://%s:%i' % self.address)
306 self.socket.connect('tcp://%s:%i' % self.address)
271 self.ioloop = ioloop.IOLoop()
307 self.ioloop = ioloop.IOLoop()
272 self.iostate = POLLIN|POLLERR
308 self.iostate = POLLIN|POLLERR
273 self.ioloop.add_handler(self.socket, self._handle_events,
309 self.ioloop.add_handler(self.socket, self._handle_events,
274 self.iostate)
310 self.iostate)
275 self.ioloop.start()
311 self.ioloop.start()
276
312
277 def stop(self):
313 def stop(self):
278 self.ioloop.stop()
314 self.ioloop.stop()
279 super(SubSocketChannel, self).stop()
315 super(SubSocketChannel, self).stop()
280
316
281 def call_handlers(self, msg):
317 def call_handlers(self, msg):
282 """This method is called in the ioloop thread when a message arrives.
318 """This method is called in the ioloop thread when a message arrives.
283
319
284 Subclasses should override this method to handle incoming messages.
320 Subclasses should override this method to handle incoming messages.
285 It is important to remember that this method is called in the thread
321 It is important to remember that this method is called in the thread
286 so that some logic must be done to ensure that the application leve
322 so that some logic must be done to ensure that the application leve
287 handlers are called in the application thread.
323 handlers are called in the application thread.
288 """
324 """
289 raise NotImplementedError('call_handlers must be defined in a subclass.')
325 raise NotImplementedError('call_handlers must be defined in a subclass.')
290
326
291 def flush(self, timeout=1.0):
327 def flush(self, timeout=1.0):
292 """Immediately processes all pending messages on the SUB channel.
328 """Immediately processes all pending messages on the SUB channel.
293
329
294 Callers should use this method to ensure that :method:`call_handlers`
330 Callers should use this method to ensure that :method:`call_handlers`
295 has been called for all messages that have been received on the
331 has been called for all messages that have been received on the
296 0MQ SUB socket of this channel.
332 0MQ SUB socket of this channel.
297
333
298 This method is thread safe.
334 This method is thread safe.
299
335
300 Parameters
336 Parameters
301 ----------
337 ----------
302 timeout : float, optional
338 timeout : float, optional
303 The maximum amount of time to spend flushing, in seconds. The
339 The maximum amount of time to spend flushing, in seconds. The
304 default is one second.
340 default is one second.
305 """
341 """
306 # We do the IOLoop callback process twice to ensure that the IOLoop
342 # We do the IOLoop callback process twice to ensure that the IOLoop
307 # gets to perform at least one full poll.
343 # gets to perform at least one full poll.
308 stop_time = time.time() + timeout
344 stop_time = time.time() + timeout
309 for i in xrange(2):
345 for i in xrange(2):
310 self._flushed = False
346 self._flushed = False
311 self.ioloop.add_callback(self._flush)
347 self.ioloop.add_callback(self._flush)
312 while not self._flushed and time.time() < stop_time:
348 while not self._flushed and time.time() < stop_time:
313 time.sleep(0.01)
349 time.sleep(0.01)
314
350
315 def _handle_events(self, socket, events):
351 def _handle_events(self, socket, events):
316 # Turn on and off POLLOUT depending on if we have made a request
352 # Turn on and off POLLOUT depending on if we have made a request
317 if events & POLLERR:
353 if events & POLLERR:
318 self._handle_err()
354 self._handle_err()
319 if events & POLLIN:
355 if events & POLLIN:
320 self._handle_recv()
356 self._handle_recv()
321
357
322 def _handle_err(self):
358 def _handle_err(self):
323 # We don't want to let this go silently, so eventually we should log.
359 # We don't want to let this go silently, so eventually we should log.
324 raise zmq.ZMQError()
360 raise zmq.ZMQError()
325
361
326 def _handle_recv(self):
362 def _handle_recv(self):
327 # Get all of the messages we can
363 # Get all of the messages we can
328 while True:
364 while True:
329 try:
365 try:
330 msg = self.socket.recv_json(zmq.NOBLOCK)
366 msg = self.socket.recv_json(zmq.NOBLOCK)
331 except zmq.ZMQError:
367 except zmq.ZMQError:
332 # Check the errno?
368 # Check the errno?
333 # Will this trigger POLLERR?
369 # Will this trigger POLLERR?
334 break
370 break
335 else:
371 else:
336 self.call_handlers(msg)
372 self.call_handlers(msg)
337
373
338 def _flush(self):
374 def _flush(self):
339 """Callback for :method:`self.flush`."""
375 """Callback for :method:`self.flush`."""
340 self._flushed = True
376 self._flushed = True
341
377
342
378
343 class RepSocketChannel(ZmqSocketChannel):
379 class RepSocketChannel(ZmqSocketChannel):
344 """A reply channel to handle raw_input requests that the kernel makes."""
380 """A reply channel to handle raw_input requests that the kernel makes."""
345
381
346 msg_queue = None
382 msg_queue = None
347
383
348 def __init__(self, context, session, address):
384 def __init__(self, context, session, address):
349 self.msg_queue = Queue()
385 self.msg_queue = Queue()
350 super(RepSocketChannel, self).__init__(context, session, address)
386 super(RepSocketChannel, self).__init__(context, session, address)
351
387
352 def run(self):
388 def run(self):
353 """The thread's main activity. Call start() instead."""
389 """The thread's main activity. Call start() instead."""
354 self.socket = self.context.socket(zmq.XREQ)
390 self.socket = self.context.socket(zmq.XREQ)
355 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
391 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
356 self.socket.connect('tcp://%s:%i' % self.address)
392 self.socket.connect('tcp://%s:%i' % self.address)
357 self.ioloop = ioloop.IOLoop()
393 self.ioloop = ioloop.IOLoop()
358 self.iostate = POLLERR|POLLIN
394 self.iostate = POLLERR|POLLIN
359 self.ioloop.add_handler(self.socket, self._handle_events,
395 self.ioloop.add_handler(self.socket, self._handle_events,
360 self.iostate)
396 self.iostate)
361 self.ioloop.start()
397 self.ioloop.start()
362
398
363 def stop(self):
399 def stop(self):
364 self.ioloop.stop()
400 self.ioloop.stop()
365 super(RepSocketChannel, self).stop()
401 super(RepSocketChannel, self).stop()
366
402
367 def call_handlers(self, msg):
403 def call_handlers(self, msg):
368 """This method is called in the ioloop thread when a message arrives.
404 """This method is called in the ioloop thread when a message arrives.
369
405
370 Subclasses should override this method to handle incoming messages.
406 Subclasses should override this method to handle incoming messages.
371 It is important to remember that this method is called in the thread
407 It is important to remember that this method is called in the thread
372 so that some logic must be done to ensure that the application leve
408 so that some logic must be done to ensure that the application leve
373 handlers are called in the application thread.
409 handlers are called in the application thread.
374 """
410 """
375 raise NotImplementedError('call_handlers must be defined in a subclass.')
411 raise NotImplementedError('call_handlers must be defined in a subclass.')
376
412
377 def input(self, string):
413 def input(self, string):
378 """Send a string of raw input to the kernel."""
414 """Send a string of raw input to the kernel."""
379 content = dict(value=string)
415 content = dict(value=string)
380 msg = self.session.msg('input_reply', content)
416 msg = self.session.msg('input_reply', content)
381 self._queue_reply(msg)
417 self._queue_reply(msg)
382
418
383 def _handle_events(self, socket, events):
419 def _handle_events(self, socket, events):
384 if events & POLLERR:
420 if events & POLLERR:
385 self._handle_err()
421 self._handle_err()
386 if events & POLLOUT:
422 if events & POLLOUT:
387 self._handle_send()
423 self._handle_send()
388 if events & POLLIN:
424 if events & POLLIN:
389 self._handle_recv()
425 self._handle_recv()
390
426
391 def _handle_recv(self):
427 def _handle_recv(self):
392 msg = self.socket.recv_json()
428 msg = self.socket.recv_json()
393 self.call_handlers(msg)
429 self.call_handlers(msg)
394
430
395 def _handle_send(self):
431 def _handle_send(self):
396 try:
432 try:
397 msg = self.msg_queue.get(False)
433 msg = self.msg_queue.get(False)
398 except Empty:
434 except Empty:
399 pass
435 pass
400 else:
436 else:
401 self.socket.send_json(msg)
437 self.socket.send_json(msg)
402 if self.msg_queue.empty():
438 if self.msg_queue.empty():
403 self.drop_io_state(POLLOUT)
439 self.drop_io_state(POLLOUT)
404
440
405 def _handle_err(self):
441 def _handle_err(self):
406 # We don't want to let this go silently, so eventually we should log.
442 # We don't want to let this go silently, so eventually we should log.
407 raise zmq.ZMQError()
443 raise zmq.ZMQError()
408
444
409 def _queue_reply(self, msg):
445 def _queue_reply(self, msg):
410 self.msg_queue.put(msg)
446 self.msg_queue.put(msg)
411 self.add_io_state(POLLOUT)
447 self.add_io_state(POLLOUT)
412
448
413
449
414 #-----------------------------------------------------------------------------
450 #-----------------------------------------------------------------------------
415 # Main kernel manager class
451 # Main kernel manager class
416 #-----------------------------------------------------------------------------
452 #-----------------------------------------------------------------------------
417
453
418 class KernelManager(HasTraits):
454 class KernelManager(HasTraits):
419 """ Manages a kernel for a frontend.
455 """ Manages a kernel for a frontend.
420
456
421 The SUB channel is for the frontend to receive messages published by the
457 The SUB channel is for the frontend to receive messages published by the
422 kernel.
458 kernel.
423
459
424 The REQ channel is for the frontend to make requests of the kernel.
460 The REQ channel is for the frontend to make requests of the kernel.
425
461
426 The REP channel is for the kernel to request stdin (raw_input) from the
462 The REP channel is for the kernel to request stdin (raw_input) from the
427 frontend.
463 frontend.
428 """
464 """
429 # The PyZMQ Context to use for communication with the kernel.
465 # The PyZMQ Context to use for communication with the kernel.
430 context = Instance(zmq.Context,(),{})
466 context = Instance(zmq.Context,(),{})
431
467
432 # The Session to use for communication with the kernel.
468 # The Session to use for communication with the kernel.
433 session = Instance(Session,(),{})
469 session = Instance(Session,(),{})
434
470
435 # The kernel process with which the KernelManager is communicating.
471 # The kernel process with which the KernelManager is communicating.
436 kernel = Instance(Popen)
472 kernel = Instance(Popen)
437
473
438 # The addresses for the communication channels.
474 # The addresses for the communication channels.
439 xreq_address = TCPAddress((LOCALHOST, 0))
475 xreq_address = TCPAddress((LOCALHOST, 0))
440 sub_address = TCPAddress((LOCALHOST, 0))
476 sub_address = TCPAddress((LOCALHOST, 0))
441 rep_address = TCPAddress((LOCALHOST, 0))
477 rep_address = TCPAddress((LOCALHOST, 0))
442
478
443 # The classes to use for the various channels.
479 # The classes to use for the various channels.
444 xreq_channel_class = Type(XReqSocketChannel)
480 xreq_channel_class = Type(XReqSocketChannel)
445 sub_channel_class = Type(SubSocketChannel)
481 sub_channel_class = Type(SubSocketChannel)
446 rep_channel_class = Type(RepSocketChannel)
482 rep_channel_class = Type(RepSocketChannel)
447
483
448 # Protected traits.
484 # Protected traits.
449 _xreq_channel = Any
485 _xreq_channel = Any
450 _sub_channel = Any
486 _sub_channel = Any
451 _rep_channel = Any
487 _rep_channel = Any
452
488
453 #--------------------------------------------------------------------------
489 #--------------------------------------------------------------------------
454 # Channel management methods:
490 # Channel management methods:
455 #--------------------------------------------------------------------------
491 #--------------------------------------------------------------------------
456
492
457 def start_channels(self):
493 def start_channels(self):
458 """Starts the channels for this kernel.
494 """Starts the channels for this kernel.
459
495
460 This will create the channels if they do not exist and then start
496 This will create the channels if they do not exist and then start
461 them. If port numbers of 0 are being used (random ports) then you
497 them. If port numbers of 0 are being used (random ports) then you
462 must first call :method:`start_kernel`. If the channels have been
498 must first call :method:`start_kernel`. If the channels have been
463 stopped and you call this, :class:`RuntimeError` will be raised.
499 stopped and you call this, :class:`RuntimeError` will be raised.
464 """
500 """
465 self.xreq_channel.start()
501 self.xreq_channel.start()
466 self.sub_channel.start()
502 self.sub_channel.start()
467 self.rep_channel.start()
503 self.rep_channel.start()
468
504
469 def stop_channels(self):
505 def stop_channels(self):
470 """Stops the channels for this kernel.
506 """Stops the channels for this kernel.
471
507
472 This stops the channels by joining their threads. If the channels
508 This stops the channels by joining their threads. If the channels
473 were not started, :class:`RuntimeError` will be raised.
509 were not started, :class:`RuntimeError` will be raised.
474 """
510 """
475 self.xreq_channel.stop()
511 self.xreq_channel.stop()
476 self.sub_channel.stop()
512 self.sub_channel.stop()
477 self.rep_channel.stop()
513 self.rep_channel.stop()
478
514
479 @property
515 @property
480 def channels_running(self):
516 def channels_running(self):
481 """Are all of the channels created and running?"""
517 """Are all of the channels created and running?"""
482 return self.xreq_channel.is_alive() \
518 return self.xreq_channel.is_alive() \
483 and self.sub_channel.is_alive() \
519 and self.sub_channel.is_alive() \
484 and self.rep_channel.is_alive()
520 and self.rep_channel.is_alive()
485
521
486 #--------------------------------------------------------------------------
522 #--------------------------------------------------------------------------
487 # Kernel process management methods:
523 # Kernel process management methods:
488 #--------------------------------------------------------------------------
524 #--------------------------------------------------------------------------
489
525
490 def start_kernel(self, ipython=True, **kw):
526 def start_kernel(self, ipython=True, **kw):
491 """Starts a kernel process and configures the manager to use it.
527 """Starts a kernel process and configures the manager to use it.
492
528
493 If random ports (port=0) are being used, this method must be called
529 If random ports (port=0) are being used, this method must be called
494 before the channels are created.
530 before the channels are created.
495
531
496 Parameters:
532 Parameters:
497 -----------
533 -----------
498 ipython : bool, optional (default True)
534 ipython : bool, optional (default True)
499 Whether to use an IPython kernel instead of a plain Python kernel.
535 Whether to use an IPython kernel instead of a plain Python kernel.
500 """
536 """
501 xreq, sub, rep = self.xreq_address, self.sub_address, self.rep_address
537 xreq, sub, rep = self.xreq_address, self.sub_address, self.rep_address
502 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or rep[0] != LOCALHOST:
538 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or rep[0] != LOCALHOST:
503 raise RuntimeError("Can only launch a kernel on localhost."
539 raise RuntimeError("Can only launch a kernel on localhost."
504 "Make sure that the '*_address' attributes are "
540 "Make sure that the '*_address' attributes are "
505 "configured properly.")
541 "configured properly.")
506
542
507 if ipython:
543 if ipython:
508 from ipkernel import launch_kernel as launch
544 from ipkernel import launch_kernel as launch
509 else:
545 else:
510 from pykernel import launch_kernel as launch
546 from pykernel import launch_kernel as launch
511 self.kernel, xrep, pub, req = launch(xrep_port=xreq[1], pub_port=sub[1],
547 self.kernel, xrep, pub, req = launch(xrep_port=xreq[1], pub_port=sub[1],
512 req_port=rep[1], **kw)
548 req_port=rep[1], **kw)
513 self.xreq_address = (LOCALHOST, xrep)
549 self.xreq_address = (LOCALHOST, xrep)
514 self.sub_address = (LOCALHOST, pub)
550 self.sub_address = (LOCALHOST, pub)
515 self.rep_address = (LOCALHOST, req)
551 self.rep_address = (LOCALHOST, req)
516
552
517 @property
553 @property
518 def has_kernel(self):
554 def has_kernel(self):
519 """Returns whether a kernel process has been specified for the kernel
555 """Returns whether a kernel process has been specified for the kernel
520 manager.
556 manager.
521 """
557 """
522 return self.kernel is not None
558 return self.kernel is not None
523
559
524 def kill_kernel(self):
560 def kill_kernel(self):
525 """ Kill the running kernel. """
561 """ Kill the running kernel. """
526 if self.kernel is not None:
562 if self.kernel is not None:
527 self.kernel.kill()
563 self.kernel.kill()
528 self.kernel = None
564 self.kernel = None
529 else:
565 else:
530 raise RuntimeError("Cannot kill kernel. No kernel is running!")
566 raise RuntimeError("Cannot kill kernel. No kernel is running!")
531
567
532 def signal_kernel(self, signum):
568 def signal_kernel(self, signum):
533 """ Sends a signal to the kernel. """
569 """ Sends a signal to the kernel. """
534 if self.kernel is not None:
570 if self.kernel is not None:
535 self.kernel.send_signal(signum)
571 self.kernel.send_signal(signum)
536 else:
572 else:
537 raise RuntimeError("Cannot signal kernel. No kernel is running!")
573 raise RuntimeError("Cannot signal kernel. No kernel is running!")
538
574
539 @property
575 @property
540 def is_alive(self):
576 def is_alive(self):
541 """Is the kernel process still running?"""
577 """Is the kernel process still running?"""
542 if self.kernel is not None:
578 if self.kernel is not None:
543 if self.kernel.poll() is None:
579 if self.kernel.poll() is None:
544 return True
580 return True
545 else:
581 else:
546 return False
582 return False
547 else:
583 else:
548 # We didn't start the kernel with this KernelManager so we don't
584 # We didn't start the kernel with this KernelManager so we don't
549 # know if it is running. We should use a heartbeat for this case.
585 # know if it is running. We should use a heartbeat for this case.
550 return True
586 return True
551
587
552 #--------------------------------------------------------------------------
588 #--------------------------------------------------------------------------
553 # Channels used for communication with the kernel:
589 # Channels used for communication with the kernel:
554 #--------------------------------------------------------------------------
590 #--------------------------------------------------------------------------
555
591
556 @property
592 @property
557 def xreq_channel(self):
593 def xreq_channel(self):
558 """Get the REQ socket channel object to make requests of the kernel."""
594 """Get the REQ socket channel object to make requests of the kernel."""
559 if self._xreq_channel is None:
595 if self._xreq_channel is None:
560 self._xreq_channel = self.xreq_channel_class(self.context,
596 self._xreq_channel = self.xreq_channel_class(self.context,
561 self.session,
597 self.session,
562 self.xreq_address)
598 self.xreq_address)
563 return self._xreq_channel
599 return self._xreq_channel
564
600
565 @property
601 @property
566 def sub_channel(self):
602 def sub_channel(self):
567 """Get the SUB socket channel object."""
603 """Get the SUB socket channel object."""
568 if self._sub_channel is None:
604 if self._sub_channel is None:
569 self._sub_channel = self.sub_channel_class(self.context,
605 self._sub_channel = self.sub_channel_class(self.context,
570 self.session,
606 self.session,
571 self.sub_address)
607 self.sub_address)
572 return self._sub_channel
608 return self._sub_channel
573
609
574 @property
610 @property
575 def rep_channel(self):
611 def rep_channel(self):
576 """Get the REP socket channel object to handle stdin (raw_input)."""
612 """Get the REP socket channel object to handle stdin (raw_input)."""
577 if self._rep_channel is None:
613 if self._rep_channel is None:
578 self._rep_channel = self.rep_channel_class(self.context,
614 self._rep_channel = self.rep_channel_class(self.context,
579 self.session,
615 self.session,
580 self.rep_address)
616 self.rep_address)
581 return self._rep_channel
617 return self._rep_channel
@@ -1,590 +1,587 b''
1 .. _messaging:
1 .. _messaging:
2
2
3 ======================
3 ======================
4 Messaging in IPython
4 Messaging in IPython
5 ======================
5 ======================
6
6
7
7
8 Introduction
8 Introduction
9 ============
9 ============
10
10
11 This document explains the basic communications design and messaging
11 This document explains the basic communications design and messaging
12 specification for how the various IPython objects interact over a network
12 specification for how the various IPython objects interact over a network
13 transport. The current implementation uses the ZeroMQ_ library for messaging
13 transport. The current implementation uses the ZeroMQ_ library for messaging
14 within and between hosts.
14 within and between hosts.
15
15
16 .. Note::
16 .. Note::
17
17
18 This document should be considered the authoritative description of the
18 This document should be considered the authoritative description of the
19 IPython messaging protocol, and all developers are strongly encouraged to
19 IPython messaging protocol, and all developers are strongly encouraged to
20 keep it updated as the implementation evolves, so that we have a single
20 keep it updated as the implementation evolves, so that we have a single
21 common reference for all protocol details.
21 common reference for all protocol details.
22
22
23 The basic design is explained in the following diagram:
23 The basic design is explained in the following diagram:
24
24
25 .. image:: frontend-kernel.png
25 .. image:: frontend-kernel.png
26 :width: 450px
26 :width: 450px
27 :alt: IPython kernel/frontend messaging architecture.
27 :alt: IPython kernel/frontend messaging architecture.
28 :align: center
28 :align: center
29 :target: ../_images/frontend-kernel.png
29 :target: ../_images/frontend-kernel.png
30
30
31 A single kernel can be simultaneously connected to one or more frontends. The
31 A single kernel can be simultaneously connected to one or more frontends. The
32 kernel has three sockets that serve the following functions:
32 kernel has three sockets that serve the following functions:
33
33
34 1. REQ: this socket is connected to a *single* frontend at a time, and it allows
34 1. REQ: this socket is connected to a *single* frontend at a time, and it allows
35 the kernel to request input from a frontend when :func:`raw_input` is called.
35 the kernel to request input from a frontend when :func:`raw_input` is called.
36 The frontend holding the matching REP socket acts as a 'virtual keyboard'
36 The frontend holding the matching REP socket acts as a 'virtual keyboard'
37 for the kernel while this communication is happening (illustrated in the
37 for the kernel while this communication is happening (illustrated in the
38 figure by the black outline around the central keyboard). In practice,
38 figure by the black outline around the central keyboard). In practice,
39 frontends may display such kernel requests using a special input widget or
39 frontends may display such kernel requests using a special input widget or
40 otherwise indicating that the user is to type input for the kernel instead
40 otherwise indicating that the user is to type input for the kernel instead
41 of normal commands in the frontend.
41 of normal commands in the frontend.
42
42
43 2. XREP: this single sockets allows multiple incoming connections from
43 2. XREP: this single sockets allows multiple incoming connections from
44 frontends, and this is the socket where requests for code execution, object
44 frontends, and this is the socket where requests for code execution, object
45 information, prompts, etc. are made to the kernel by any frontend. The
45 information, prompts, etc. are made to the kernel by any frontend. The
46 communication on this socket is a sequence of request/reply actions from
46 communication on this socket is a sequence of request/reply actions from
47 each frontend and the kernel.
47 each frontend and the kernel.
48
48
49 3. PUB: this socket is the 'broadcast channel' where the kernel publishes all
49 3. PUB: this socket is the 'broadcast channel' where the kernel publishes all
50 side effects (stdout, stderr, etc.) as well as the requests coming from any
50 side effects (stdout, stderr, etc.) as well as the requests coming from any
51 client over the XREP socket and its own requests on the REP socket. There
51 client over the XREP socket and its own requests on the REP socket. There
52 are a number of actions in Python which generate side effects: :func:`print`
52 are a number of actions in Python which generate side effects: :func:`print`
53 writes to ``sys.stdout``, errors generate tracebacks, etc. Additionally, in
53 writes to ``sys.stdout``, errors generate tracebacks, etc. Additionally, in
54 a multi-client scenario, we want all frontends to be able to know what each
54 a multi-client scenario, we want all frontends to be able to know what each
55 other has sent to the kernel (this can be useful in collaborative scenarios,
55 other has sent to the kernel (this can be useful in collaborative scenarios,
56 for example). This socket allows both side effects and the information
56 for example). This socket allows both side effects and the information
57 about communications taking place with one client over the XREQ/XREP channel
57 about communications taking place with one client over the XREQ/XREP channel
58 to be made available to all clients in a uniform manner.
58 to be made available to all clients in a uniform manner.
59
59
60 All messages are tagged with enough information (details below) for clients
60 All messages are tagged with enough information (details below) for clients
61 to know which messages come from their own interaction with the kernel and
61 to know which messages come from their own interaction with the kernel and
62 which ones are from other clients, so they can display each type
62 which ones are from other clients, so they can display each type
63 appropriately.
63 appropriately.
64
64
65 The actual format of the messages allowed on each of these channels is
65 The actual format of the messages allowed on each of these channels is
66 specified below. Messages are dicts of dicts with string keys and values that
66 specified below. Messages are dicts of dicts with string keys and values that
67 are reasonably representable in JSON. Our current implementation uses JSON
67 are reasonably representable in JSON. Our current implementation uses JSON
68 explicitly as its message format, but this shouldn't be considered a permanent
68 explicitly as its message format, but this shouldn't be considered a permanent
69 feature. As we've discovered that JSON has non-trivial performance issues due
69 feature. As we've discovered that JSON has non-trivial performance issues due
70 to excessive copying, we may in the future move to a pure pickle-based raw
70 to excessive copying, we may in the future move to a pure pickle-based raw
71 message format. However, it should be possible to easily convert from the raw
71 message format. However, it should be possible to easily convert from the raw
72 objects to JSON, since we may have non-python clients (e.g. a web frontend).
72 objects to JSON, since we may have non-python clients (e.g. a web frontend).
73 As long as it's easy to make a JSON version of the objects that is a faithful
73 As long as it's easy to make a JSON version of the objects that is a faithful
74 representation of all the data, we can communicate with such clients.
74 representation of all the data, we can communicate with such clients.
75
75
76 .. Note::
76 .. Note::
77
77
78 Not all of these have yet been fully fleshed out, but the key ones are, see
78 Not all of these have yet been fully fleshed out, but the key ones are, see
79 kernel and frontend files for actual implementation details.
79 kernel and frontend files for actual implementation details.
80
80
81
81
82 Python functional API
82 Python functional API
83 =====================
83 =====================
84
84
85 As messages are dicts, they map naturally to a ``func(**kw)`` call form. We
85 As messages are dicts, they map naturally to a ``func(**kw)`` call form. We
86 should develop, at a few key points, functional forms of all the requests that
86 should develop, at a few key points, functional forms of all the requests that
87 take arguments in this manner and automatically construct the necessary dict
87 take arguments in this manner and automatically construct the necessary dict
88 for sending.
88 for sending.
89
89
90
90
91 General Message Format
91 General Message Format
92 ======================
92 ======================
93
93
94 All messages send or received by any IPython process should have the following
94 All messages send or received by any IPython process should have the following
95 generic structure::
95 generic structure::
96
96
97 {
97 {
98 # The message header contains a pair of unique identifiers for the
98 # The message header contains a pair of unique identifiers for the
99 # originating session and the actual message id, in addition to the
99 # originating session and the actual message id, in addition to the
100 # username for the process that generated the message. This is useful in
100 # username for the process that generated the message. This is useful in
101 # collaborative settings where multiple users may be interacting with the
101 # collaborative settings where multiple users may be interacting with the
102 # same kernel simultaneously, so that frontends can label the various
102 # same kernel simultaneously, so that frontends can label the various
103 # messages in a meaningful way.
103 # messages in a meaningful way.
104 'header' : { 'msg_id' : uuid,
104 'header' : { 'msg_id' : uuid,
105 'username' : str,
105 'username' : str,
106 'session' : uuid
106 'session' : uuid
107 },
107 },
108
108
109 # In a chain of messages, the header from the parent is copied so that
109 # In a chain of messages, the header from the parent is copied so that
110 # clients can track where messages come from.
110 # clients can track where messages come from.
111 'parent_header' : dict,
111 'parent_header' : dict,
112
112
113 # All recognized message type strings are listed below.
113 # All recognized message type strings are listed below.
114 'msg_type' : str,
114 'msg_type' : str,
115
115
116 # The actual content of the message must be a dict, whose structure
116 # The actual content of the message must be a dict, whose structure
117 # depends on the message type.x
117 # depends on the message type.x
118 'content' : dict,
118 'content' : dict,
119 }
119 }
120
120
121 For each message type, the actual content will differ and all existing message
121 For each message type, the actual content will differ and all existing message
122 types are specified in what follows of this document.
122 types are specified in what follows of this document.
123
123
124
124
125 Messages on the XREP/XREQ socket
125 Messages on the XREP/XREQ socket
126 ================================
126 ================================
127
127
128 .. _execute:
128 .. _execute:
129
129
130 Execute
130 Execute
131 -------
131 -------
132
132
133 The execution request contains a single string, but this may be a multiline
133 The execution request contains a single string, but this may be a multiline
134 string. The kernel is responsible for splitting this into possibly more than
134 string. The kernel is responsible for splitting this into possibly more than
135 one block and deciding whether to compile these in 'single' or 'exec' mode.
135 one block and deciding whether to compile these in 'single' or 'exec' mode.
136 We're still sorting out this policy. The current inputsplitter is capable of
136 We're still sorting out this policy. The current inputsplitter is capable of
137 splitting the input for blocks that can all be run as 'single', but in the long
137 splitting the input for blocks that can all be run as 'single', but in the long
138 run it may prove cleaner to only use 'single' mode for truly single-line
138 run it may prove cleaner to only use 'single' mode for truly single-line
139 inputs, and run all multiline input in 'exec' mode. This would preserve the
139 inputs, and run all multiline input in 'exec' mode. This would preserve the
140 natural behavior of single-line inputs while allowing long cells to behave more
140 natural behavior of single-line inputs while allowing long cells to behave more
141 likea a script. This design will be refined as we complete the implementation.
141 likea a script. This design will be refined as we complete the implementation.
142
142
143 Message type: ``execute_request``::
143 Message type: ``execute_request``::
144
144
145 content = {
145 content = {
146 # Source code to be executed by the kernel, one or more lines.
146 # Source code to be executed by the kernel, one or more lines.
147 'code' : str,
147 'code' : str,
148
148
149 # A boolean flag which, if True, signals the kernel to execute this
149 # A boolean flag which, if True, signals the kernel to execute this
150 # code as quietly as possible. This means that the kernel will compile
150 # code as quietly as possible. This means that the kernel will compile
151 # the code with 'exec' instead of 'single' (so sys.displayhook will not
151 # the code with 'exec' instead of 'single' (so sys.displayhook will not
152 # fire), and will *not*:
152 # fire), and will *not*:
153 # - broadcast exceptions on the PUB socket
153 # - broadcast exceptions on the PUB socket
154 # - do any logging
154 # - do any logging
155 # - populate any history
155 # - populate any history
156 # The default is False.
156 # The default is False.
157 'silent' : bool,
157 'silent' : bool,
158 }
158 }
159
159
160 Upon execution, the kernel *always* sends a reply, with a status code
160 Upon execution, the kernel *always* sends a reply, with a status code
161 indicating what happened and additional data depending on the outcome.
161 indicating what happened and additional data depending on the outcome.
162
162
163 Message type: ``execute_reply``::
163 Message type: ``execute_reply``::
164
164
165 content = {
165 content = {
166 # One of: 'ok' OR 'error' OR 'abort'
166 # One of: 'ok' OR 'error' OR 'abort'
167 'status' : str,
167 'status' : str,
168
168
169 # This has the same structure as the output of a prompt request, but is
169 # This has the same structure as the output of a prompt request, but is
170 # for the client to set up the *next* prompt (with identical limitations
170 # for the client to set up the *next* prompt (with identical limitations
171 # to a prompt request)
171 # to a prompt request)
172 'next_prompt' : {
172 'next_prompt' : {
173 'prompt_string' : str,
173 'prompt_string' : str,
174 'prompt_number' : int,
174 'prompt_number' : int,
175 'input_sep' : str
175 'input_sep' : str
176 },
176 },
177
177
178 # The prompt number of the actual execution for this code, which may be
178 # The prompt number of the actual execution for this code, which may be
179 # different from the one used when the code was typed, which was the
179 # different from the one used when the code was typed, which was the
180 # 'next_prompt' field of the *previous* request. They will differ in the
180 # 'next_prompt' field of the *previous* request. They will differ in the
181 # case where there is more than one client talking simultaneously to a
181 # case where there is more than one client talking simultaneously to a
182 # kernel, since the numbers can go out of sync. GUI clients can use this
182 # kernel, since the numbers can go out of sync. GUI clients can use this
183 # to correct the previously written number in-place, terminal ones may
183 # to correct the previously written number in-place, terminal ones may
184 # re-print a corrected one if desired.
184 # re-print a corrected one if desired.
185 'prompt_number' : int,
185 'prompt_number' : int,
186 }
186 }
187
187
188 When status is 'ok', the following extra fields are present::
188 When status is 'ok', the following extra fields are present::
189
189
190 {
190 {
191 # The kernel will often transform the input provided to it. This
191 # The kernel will often transform the input provided to it. This
192 # contains the transformed code, which is what was actually executed.
192 # contains the transformed code, which is what was actually executed.
193 'transformed_code' : str,
193 'transformed_code' : str,
194
194
195 # The execution payload is a dict with string keys that may have been
195 # The execution payload is a dict with string keys that may have been
196 # produced by the code being executed. It is retrieved by the kernel at
196 # produced by the code being executed. It is retrieved by the kernel at
197 # the end of the execution and sent back to the front end, which can take
197 # the end of the execution and sent back to the front end, which can take
198 # action on it as needed. See main text for further details.
198 # action on it as needed. See main text for further details.
199 'payload' : dict,
199 'payload' : dict,
200 }
200 }
201
201
202 .. admonition:: Execution payloads
202 .. admonition:: Execution payloads
203
203
204 The notion of an 'execution payload' is different from a return value of a
204 The notion of an 'execution payload' is different from a return value of a
205 given set of code, which normally is just displayed on the pyout stream
205 given set of code, which normally is just displayed on the pyout stream
206 through the PUB socket. The idea of a payload is to allow special types of
206 through the PUB socket. The idea of a payload is to allow special types of
207 code, typically magics, to populate a data container in the IPython kernel
207 code, typically magics, to populate a data container in the IPython kernel
208 that will be shipped back to the caller via this channel. The kernel will
208 that will be shipped back to the caller via this channel. The kernel will
209 have an API for this, probably something along the lines of::
209 have an API for this, probably something along the lines of::
210
210
211 ip.exec_payload_add(key, value)
211 ip.exec_payload_add(key, value)
212
212
213 though this API is still in the design stages. The data returned in this
213 though this API is still in the design stages. The data returned in this
214 payload will allow frontends to present special views of what just happened.
214 payload will allow frontends to present special views of what just happened.
215
215
216
216
217 When status is 'error', the following extra fields are present::
217 When status is 'error', the following extra fields are present::
218
218
219 {
219 {
220 'exc_name' : str, # Exception name, as a string
220 'exc_name' : str, # Exception name, as a string
221 'exc_value' : str, # Exception value, as a string
221 'exc_value' : str, # Exception value, as a string
222
222
223 # The traceback will contain a list of frames, represented each as a
223 # The traceback will contain a list of frames, represented each as a
224 # string. For now we'll stick to the existing design of ultraTB, which
224 # string. For now we'll stick to the existing design of ultraTB, which
225 # controls exception level of detail statefully. But eventually we'll
225 # controls exception level of detail statefully. But eventually we'll
226 # want to grow into a model where more information is collected and
226 # want to grow into a model where more information is collected and
227 # packed into the traceback object, with clients deciding how little or
227 # packed into the traceback object, with clients deciding how little or
228 # how much of it to unpack. But for now, let's start with a simple list
228 # how much of it to unpack. But for now, let's start with a simple list
229 # of strings, since that requires only minimal changes to ultratb as
229 # of strings, since that requires only minimal changes to ultratb as
230 # written.
230 # written.
231 'traceback' : list,
231 'traceback' : list,
232 }
232 }
233
233
234
234
235 When status is 'abort', there are for now no additional data fields. This
235 When status is 'abort', there are for now no additional data fields. This
236 happens when the kernel was interrupted by a signal.
236 happens when the kernel was interrupted by a signal.
237
237
238
238
239 Prompt
239 Prompt
240 ------
240 ------
241
241
242 A simple request for a current prompt string.
242 A simple request for a current prompt string.
243
243
244 Message type: ``prompt_request``::
244 Message type: ``prompt_request``::
245
245
246 content = {}
246 content = {}
247
247
248 In the reply, the prompt string comes back with the prompt number placeholder
248 In the reply, the prompt string comes back with the prompt number placeholder
249 *unevaluated*. The message format is:
249 *unevaluated*. The message format is:
250
250
251 Message type: ``prompt_reply``::
251 Message type: ``prompt_reply``::
252
252
253 content = {
253 content = {
254 'prompt_string' : str,
254 'prompt_string' : str,
255 'prompt_number' : int,
255 'prompt_number' : int,
256 'input_sep' : str
256 }
257 }
257
258
258 Clients can produce a prompt with ``prompt_string.format(prompt_number)``, but
259 Clients can produce a prompt with ``prompt_string.format(prompt_number)``, but
259 they should be aware that the actual prompt number for that input could change
260 they should be aware that the actual prompt number for that input could change
260 later, in the case where multiple clients are interacting with a single
261 later, in the case where multiple clients are interacting with a single
261 kernel.
262 kernel.
262
263
263
264
264 Object information
265 Object information
265 ------------------
266 ------------------
266
267
267 One of IPython's most used capabilities is the introspection of Python objects
268 One of IPython's most used capabilities is the introspection of Python objects
268 in the user's namespace, typically invoked via the ``?`` and ``??`` characters
269 in the user's namespace, typically invoked via the ``?`` and ``??`` characters
269 (which in reality are shorthands for the ``%pinfo`` magic). This is used often
270 (which in reality are shorthands for the ``%pinfo`` magic). This is used often
270 enough that it warrants an explicit message type, especially because frontends
271 enough that it warrants an explicit message type, especially because frontends
271 may want to get object information in response to user keystrokes (like Tab or
272 may want to get object information in response to user keystrokes (like Tab or
272 F1) besides from the user explicitly typing code like ``x??``.
273 F1) besides from the user explicitly typing code like ``x??``.
273
274
274 Message type: ``object_info_request``::
275 Message type: ``object_info_request``::
275
276
276 content = {
277 content = {
277 # The (possibly dotted) name of the object to be searched in all
278 # The (possibly dotted) name of the object to be searched in all
278 # relevant namespaces
279 # relevant namespaces
279 'name' : str,
280 'name' : str,
280
281
281 # The level of detail desired. The default (0) is equivalent to typing
282 # The level of detail desired. The default (0) is equivalent to typing
282 # 'x?' at the prompt, 1 is equivalent to 'x??'.
283 # 'x?' at the prompt, 1 is equivalent to 'x??'.
283 'detail_level' : int,
284 'detail_level' : int,
284 }
285 }
285
286
286 The returned information will be a dictionary with keys very similar to the
287 The returned information will be a dictionary with keys very similar to the
287 field names that IPython prints at the terminal.
288 field names that IPython prints at the terminal.
288
289
289 Message type: ``object_info_reply``::
290 Message type: ``object_info_reply``::
290
291
291 content = {
292 content = {
292 # Flags for magics and system aliases
293 # Flags for magics and system aliases
293 'ismagic' : bool,
294 'ismagic' : bool,
294 'isalias' : bool,
295 'isalias' : bool,
295
296
296 # The name of the namespace where the object was found ('builtin',
297 # The name of the namespace where the object was found ('builtin',
297 # 'magics', 'alias', 'interactive', etc.)
298 # 'magics', 'alias', 'interactive', etc.)
298 'namespace' : str,
299 'namespace' : str,
299
300
300 # The type name will be type.__name__ for normal Python objects, but it
301 # The type name will be type.__name__ for normal Python objects, but it
301 # can also be a string like 'Magic function' or 'System alias'
302 # can also be a string like 'Magic function' or 'System alias'
302 'type_name' : str,
303 'type_name' : str,
303
304
304 'string_form' : str,
305 'string_form' : str,
305
306
306 # For objects with a __class__ attribute this will be set
307 # For objects with a __class__ attribute this will be set
307 'base_class' : str,
308 'base_class' : str,
308
309
309 # For objects with a __len__ attribute this will be set
310 # For objects with a __len__ attribute this will be set
310 'length' : int,
311 'length' : int,
311
312
312 # If the object is a function, class or method whose file we can find,
313 # If the object is a function, class or method whose file we can find,
313 # we give its full path
314 # we give its full path
314 'file' : str,
315 'file' : str,
315
316
316 # For pure Python callable objects, we can reconstruct the object
317 # For pure Python callable objects, we can reconstruct the object
317 # definition line which provides its call signature
318 # definition line which provides its call signature
318 'definition' : str,
319 'definition' : str,
319
320
320 # For instances, provide the constructor signature (the definition of
321 # For instances, provide the constructor signature (the definition of
321 # the __init__ method):
322 # the __init__ method):
322 'init_definition' : str,
323 'init_definition' : str,
323
324
324 # Docstrings: for any object (function, method, module, package) with a
325 # Docstrings: for any object (function, method, module, package) with a
325 # docstring, we show it. But in addition, we may provide additional
326 # docstring, we show it. But in addition, we may provide additional
326 # docstrings. For example, for instances we will show the constructor
327 # docstrings. For example, for instances we will show the constructor
327 # and class docstrings as well, if available.
328 # and class docstrings as well, if available.
328 'docstring' : str,
329 'docstring' : str,
329
330
330 # For instances, provide the constructor and class docstrings
331 # For instances, provide the constructor and class docstrings
331 'init_docstring' : str,
332 'init_docstring' : str,
332 'class_docstring' : str,
333 'class_docstring' : str,
333
334
334 # If detail_level was 1, we also try to find the source code that
335 # If detail_level was 1, we also try to find the source code that
335 # defines the object, if possible. The string 'None' will indicate
336 # defines the object, if possible. The string 'None' will indicate
336 # that no source was found.
337 # that no source was found.
337 'source' : str,
338 'source' : str,
338 }
339 }
339
340
340
341
341 Complete
342 Complete
342 --------
343 --------
343
344
344 Message type: ``complete_request``::
345 Message type: ``complete_request``::
345
346
346 content = {
347 content = {
347 # The text to be completed, such as 'a.is'
348 # The text to be completed, such as 'a.is'
348 'text' : str,
349 'text' : str,
349
350
350 # The full line, such as 'print a.is'. This allows completers to
351 # The full line, such as 'print a.is'. This allows completers to
351 # make decisions that may require information about more than just the
352 # make decisions that may require information about more than just the
352 # current word.
353 # current word.
353 'line' : str,
354 'line' : str,
354
355
355 # The entire block of text where the line is. This may be useful in the
356 # The entire block of text where the line is. This may be useful in the
356 # case of multiline completions where more context may be needed. Note: if
357 # case of multiline completions where more context may be needed. Note: if
357 # in practice this field proves unnecessary, remove it to lighten the
358 # in practice this field proves unnecessary, remove it to lighten the
358 # messages.
359 # messages.
359
360
360 'block' : str,
361 'block' : str,
361
362
362 # The position of the cursor where the user hit 'TAB' on the line.
363 # The position of the cursor where the user hit 'TAB' on the line.
363 'cursor_pos' : int,
364 'cursor_pos' : int,
364 }
365 }
365
366
366 Message type: ``complete_reply``::
367 Message type: ``complete_reply``::
367
368
368 content = {
369 content = {
369 # The list of all matches to the completion request, such as
370 # The list of all matches to the completion request, such as
370 # ['a.isalnum', 'a.isalpha'] for the above example.
371 # ['a.isalnum', 'a.isalpha'] for the above example.
371 'matches' : list
372 'matches' : list
372 }
373 }
373
374
374
375
375 History
376 History
376 -------
377 -------
377
378
378 For clients to explicitly request history from a kernel. The kernel has all
379 For clients to explicitly request history from a kernel. The kernel has all
379 the actual execution history stored in a single location, so clients can
380 the actual execution history stored in a single location, so clients can
380 request it from the kernel when needed.
381 request it from the kernel when needed.
381
382
382 Message type: ``history_request``::
383 Message type: ``history_request``::
383
384
384 content = {
385 content = {
385
386
386 # If True, also return output history in the resulting dict.
387 # If True, also return output history in the resulting dict.
387 'output' : bool,
388 'output' : bool,
388
389
389 # If True, return the raw input history, else the transformed input.
390 # If True, return the raw input history, else the transformed input.
390 'raw' : bool,
391 'raw' : bool,
391
392
392 # This parameter can be one of: A number, a pair of numbers, None
393 # This parameter can be one of: A number, a pair of numbers, None
393 # If not given, last 40 are returned.
394 # If not given, last 40 are returned.
394 # - number n: return the last n entries.
395 # - number n: return the last n entries.
395 # - pair n1, n2: return entries in the range(n1, n2).
396 # - pair n1, n2: return entries in the range(n1, n2).
396 # - None: return all history
397 # - None: return all history
397 'range' : n or (n1, n2) or None,
398 'index' : n or (n1, n2) or None,
398
399 # If a filter is given, it is treated as a regular expression and only
400 # matching entries are returned. re.search() is used to find matches.
401 'filter' : str,
402 }
399 }
403
400
404 Message type: ``history_reply``::
401 Message type: ``history_reply``::
405
402
406 content = {
403 content = {
407 # A dict with prompt numbers as keys and either (input, output) or input
404 # A dict with prompt numbers as keys and either (input, output) or input
408 # as the value depending on whether output was True or False,
405 # as the value depending on whether output was True or False,
409 # respectively.
406 # respectively.
410 'history' : dict,
407 'history' : dict,
411 }
408 }
412 Messages on the PUB/SUB socket
409 Messages on the PUB/SUB socket
413 ==============================
410 ==============================
414
411
415 Streams (stdout, stderr, etc)
412 Streams (stdout, stderr, etc)
416 ------------------------------
413 ------------------------------
417
414
418 Message type: ``stream``::
415 Message type: ``stream``::
419
416
420 content = {
417 content = {
421 # The name of the stream is one of 'stdin', 'stdout', 'stderr'
418 # The name of the stream is one of 'stdin', 'stdout', 'stderr'
422 'name' : str,
419 'name' : str,
423
420
424 # The data is an arbitrary string to be written to that stream
421 # The data is an arbitrary string to be written to that stream
425 'data' : str,
422 'data' : str,
426 }
423 }
427
424
428 When a kernel receives a raw_input call, it should also broadcast it on the pub
425 When a kernel receives a raw_input call, it should also broadcast it on the pub
429 socket with the names 'stdin' and 'stdin_reply'. This will allow other clients
426 socket with the names 'stdin' and 'stdin_reply'. This will allow other clients
430 to monitor/display kernel interactions and possibly replay them to their user
427 to monitor/display kernel interactions and possibly replay them to their user
431 or otherwise expose them.
428 or otherwise expose them.
432
429
433 Python inputs
430 Python inputs
434 -------------
431 -------------
435
432
436 These messages are the re-broadcast of the ``execute_request``.
433 These messages are the re-broadcast of the ``execute_request``.
437
434
438 Message type: ``pyin``::
435 Message type: ``pyin``::
439
436
440 content = {
437 content = {
441 # Source code to be executed, one or more lines
438 # Source code to be executed, one or more lines
442 'code' : str
439 'code' : str
443 }
440 }
444
441
445 Python outputs
442 Python outputs
446 --------------
443 --------------
447
444
448 When Python produces output from code that has been compiled in with the
445 When Python produces output from code that has been compiled in with the
449 'single' flag to :func:`compile`, any expression that produces a value (such as
446 'single' flag to :func:`compile`, any expression that produces a value (such as
450 ``1+1``) is passed to ``sys.displayhook``, which is a callable that can do with
447 ``1+1``) is passed to ``sys.displayhook``, which is a callable that can do with
451 this value whatever it wants. The default behavior of ``sys.displayhook`` in
448 this value whatever it wants. The default behavior of ``sys.displayhook`` in
452 the Python interactive prompt is to print to ``sys.stdout`` the :func:`repr` of
449 the Python interactive prompt is to print to ``sys.stdout`` the :func:`repr` of
453 the value as long as it is not ``None`` (which isn't printed at all). In our
450 the value as long as it is not ``None`` (which isn't printed at all). In our
454 case, the kernel instantiates as ``sys.displayhook`` an object which has
451 case, the kernel instantiates as ``sys.displayhook`` an object which has
455 similar behavior, but which instead of printing to stdout, broadcasts these
452 similar behavior, but which instead of printing to stdout, broadcasts these
456 values as ``pyout`` messages for clients to display appropriately.
453 values as ``pyout`` messages for clients to display appropriately.
457
454
458 Message type: ``pyout``::
455 Message type: ``pyout``::
459
456
460 content = {
457 content = {
461 # The data is typically the repr() of the object.
458 # The data is typically the repr() of the object.
462 'data' : str,
459 'data' : str,
463
460
464 # The prompt number for this execution is also provided so that clients
461 # The prompt number for this execution is also provided so that clients
465 # can display it, since IPython automatically creates variables called
462 # can display it, since IPython automatically creates variables called
466 # _N (for prompt N).
463 # _N (for prompt N).
467 'prompt_number' : int,
464 'prompt_number' : int,
468 }
465 }
469
466
470 Python errors
467 Python errors
471 -------------
468 -------------
472
469
473 When an error occurs during code execution
470 When an error occurs during code execution
474
471
475 Message type: ``pyerr``::
472 Message type: ``pyerr``::
476
473
477 content = {
474 content = {
478 # Similar content to the execute_reply messages for the 'error' case,
475 # Similar content to the execute_reply messages for the 'error' case,
479 # except the 'status' field is omitted.
476 # except the 'status' field is omitted.
480 }
477 }
481
478
482 Kernel crashes
479 Kernel crashes
483 --------------
480 --------------
484
481
485 When the kernel has an unexpected exception, caught by the last-resort
482 When the kernel has an unexpected exception, caught by the last-resort
486 sys.excepthook, we should broadcast the crash handler's output before exiting.
483 sys.excepthook, we should broadcast the crash handler's output before exiting.
487 This will allow clients to notice that a kernel died, inform the user and
484 This will allow clients to notice that a kernel died, inform the user and
488 propose further actions.
485 propose further actions.
489
486
490 Message type: ``crash``::
487 Message type: ``crash``::
491
488
492 content = {
489 content = {
493 # Similarly to the 'error' case for execute_reply messages, this will
490 # Similarly to the 'error' case for execute_reply messages, this will
494 # contain exc_name, exc_type and traceback fields.
491 # contain exc_name, exc_type and traceback fields.
495
492
496 # An additional field with supplementary information such as where to
493 # An additional field with supplementary information such as where to
497 # send the crash message
494 # send the crash message
498 'info' : str,
495 'info' : str,
499 }
496 }
500
497
501
498
502 Future ideas
499 Future ideas
503 ------------
500 ------------
504
501
505 Other potential message types, currently unimplemented, listed below as ideas.
502 Other potential message types, currently unimplemented, listed below as ideas.
506
503
507 Message type: ``file``::
504 Message type: ``file``::
508
505
509 content = {
506 content = {
510 'path' : 'cool.jpg',
507 'path' : 'cool.jpg',
511 'mimetype' : str,
508 'mimetype' : str,
512 'data' : str,
509 'data' : str,
513 }
510 }
514
511
515
512
516 Messages on the REQ/REP socket
513 Messages on the REQ/REP socket
517 ==============================
514 ==============================
518
515
519 This is a socket that goes in the opposite direction: from the kernel to a
516 This is a socket that goes in the opposite direction: from the kernel to a
520 *single* frontend, and its purpose is to allow ``raw_input`` and similar
517 *single* frontend, and its purpose is to allow ``raw_input`` and similar
521 operations that read from ``sys.stdin`` on the kernel to be fulfilled by the
518 operations that read from ``sys.stdin`` on the kernel to be fulfilled by the
522 client. For now we will keep these messages as simple as possible, since they
519 client. For now we will keep these messages as simple as possible, since they
523 basically only mean to convey the ``raw_input(prompt)`` call.
520 basically only mean to convey the ``raw_input(prompt)`` call.
524
521
525 Message type: ``input_request``::
522 Message type: ``input_request``::
526
523
527 content = { 'prompt' : str }
524 content = { 'prompt' : str }
528
525
529 Message type: ``input_reply``::
526 Message type: ``input_reply``::
530
527
531 content = { 'value' : str }
528 content = { 'value' : str }
532
529
533 .. Note::
530 .. Note::
534
531
535 We do not explicitly try to forward the raw ``sys.stdin`` object, because in
532 We do not explicitly try to forward the raw ``sys.stdin`` object, because in
536 practice the kernel should behave like an interactive program. When a
533 practice the kernel should behave like an interactive program. When a
537 program is opened on the console, the keyboard effectively takes over the
534 program is opened on the console, the keyboard effectively takes over the
538 ``stdin`` file descriptor, and it can't be used for raw reading anymore.
535 ``stdin`` file descriptor, and it can't be used for raw reading anymore.
539 Since the IPython kernel effectively behaves like a console program (albeit
536 Since the IPython kernel effectively behaves like a console program (albeit
540 one whose "keyboard" is actually living in a separate process and
537 one whose "keyboard" is actually living in a separate process and
541 transported over the zmq connection), raw ``stdin`` isn't expected to be
538 transported over the zmq connection), raw ``stdin`` isn't expected to be
542 available.
539 available.
543
540
544
541
545 Heartbeat for kernels
542 Heartbeat for kernels
546 =====================
543 =====================
547
544
548 Initially we had considered using messages like those above over ZMQ for a
545 Initially we had considered using messages like those above over ZMQ for a
549 kernel 'heartbeat' (a way to detect quickly and reliably whether a kernel is
546 kernel 'heartbeat' (a way to detect quickly and reliably whether a kernel is
550 alive at all, even if it may be busy executing user code). But this has the
547 alive at all, even if it may be busy executing user code). But this has the
551 problem that if the kernel is locked inside extension code, it wouldn't execute
548 problem that if the kernel is locked inside extension code, it wouldn't execute
552 the python heartbeat code. But it turns out that we can implement a basic
549 the python heartbeat code. But it turns out that we can implement a basic
553 heartbeat with pure ZMQ, without using any Python messaging at all.
550 heartbeat with pure ZMQ, without using any Python messaging at all.
554
551
555 The monitor sends out a single zmq message (right now, it is a str of the
552 The monitor sends out a single zmq message (right now, it is a str of the
556 monitor's lifetime in seconds), and gets the same message right back, prefixed
553 monitor's lifetime in seconds), and gets the same message right back, prefixed
557 with the zmq identity of the XREQ socket in the heartbeat process. This can be
554 with the zmq identity of the XREQ socket in the heartbeat process. This can be
558 a uuid, or even a full message, but there doesn't seem to be a need for packing
555 a uuid, or even a full message, but there doesn't seem to be a need for packing
559 up a message when the sender and receiver are the exact same Python object.
556 up a message when the sender and receiver are the exact same Python object.
560
557
561 The model is this::
558 The model is this::
562
559
563 monitor.send(str(self.lifetime)) # '1.2345678910'
560 monitor.send(str(self.lifetime)) # '1.2345678910'
564
561
565 and the monitor receives some number of messages of the form::
562 and the monitor receives some number of messages of the form::
566
563
567 ['uuid-abcd-dead-beef', '1.2345678910']
564 ['uuid-abcd-dead-beef', '1.2345678910']
568
565
569 where the first part is the zmq.IDENTITY of the heart's XREQ on the engine, and
566 where the first part is the zmq.IDENTITY of the heart's XREQ on the engine, and
570 the rest is the message sent by the monitor. No Python code ever has any
567 the rest is the message sent by the monitor. No Python code ever has any
571 access to the message between the monitor's send, and the monitor's recv.
568 access to the message between the monitor's send, and the monitor's recv.
572
569
573
570
574 ToDo
571 ToDo
575 ====
572 ====
576
573
577 Missing things include:
574 Missing things include:
578
575
579 * Important: finish thinking through the payload concept and API.
576 * Important: finish thinking through the payload concept and API.
580
577
581 * Important: ensure that we have a good solution for magics like %edit. It's
578 * Important: ensure that we have a good solution for magics like %edit. It's
582 likely that with the payload concept we can build a full solution, but not
579 likely that with the payload concept we can build a full solution, but not
583 100% clear yet.
580 100% clear yet.
584
581
585 * Finishing the details of the heartbeat protocol.
582 * Finishing the details of the heartbeat protocol.
586
583
587 * Signal handling: specify what kind of information kernel should broadcast (or
584 * Signal handling: specify what kind of information kernel should broadcast (or
588 not) when it receives signals.
585 not) when it receives signals.
589
586
590 .. include:: ../links.rst
587 .. include:: ../links.rst
General Comments 0
You need to be logged in to leave comments. Login now