##// END OF EJS Templates
* Moved shutdown_kernel method from FrontendWidget to KernelManager....
epatters -
Show More
@@ -1,1567 +1,1568 b''
1 # Standard library imports
1 # Standard library imports
2 from os.path import commonprefix
2 from os.path import commonprefix
3 import re
3 import re
4 import sys
4 import sys
5 from textwrap import dedent
5 from textwrap import dedent
6
6
7 # System library imports
7 # System library imports
8 from PyQt4 import QtCore, QtGui
8 from PyQt4 import QtCore, QtGui
9
9
10 # Local imports
10 # Local imports
11 from IPython.config.configurable import Configurable
11 from IPython.config.configurable import Configurable
12 from IPython.frontend.qt.util import MetaQObjectHasTraits
12 from IPython.frontend.qt.util import MetaQObjectHasTraits
13 from IPython.utils.traitlets import Bool, Enum, Int
13 from IPython.utils.traitlets import Bool, Enum, Int
14 from ansi_code_processor import QtAnsiCodeProcessor
14 from ansi_code_processor import QtAnsiCodeProcessor
15 from completion_widget import CompletionWidget
15 from completion_widget import CompletionWidget
16
16
17
17
18 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
18 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
19 """ A QPlainTextEdit suitable for use with ConsoleWidget.
19 """ A QPlainTextEdit suitable for use with ConsoleWidget.
20 """
20 """
21 # Prevents text from being moved by drag and drop. Note that is not, for
21 # Prevents text from being moved by drag and drop. Note that is not, for
22 # some reason, sufficient to catch drag events in the ConsoleWidget's
22 # some reason, sufficient to catch drag events in the ConsoleWidget's
23 # event filter.
23 # event filter.
24 def dragEnterEvent(self, event): pass
24 def dragEnterEvent(self, event): pass
25 def dragLeaveEvent(self, event): pass
25 def dragLeaveEvent(self, event): pass
26 def dragMoveEvent(self, event): pass
26 def dragMoveEvent(self, event): pass
27 def dropEvent(self, event): pass
27 def dropEvent(self, event): pass
28
28
29 class ConsoleTextEdit(QtGui.QTextEdit):
29 class ConsoleTextEdit(QtGui.QTextEdit):
30 """ A QTextEdit suitable for use with ConsoleWidget.
30 """ A QTextEdit suitable for use with ConsoleWidget.
31 """
31 """
32 # See above.
32 # See above.
33 def dragEnterEvent(self, event): pass
33 def dragEnterEvent(self, event): pass
34 def dragLeaveEvent(self, event): pass
34 def dragLeaveEvent(self, event): pass
35 def dragMoveEvent(self, event): pass
35 def dragMoveEvent(self, event): pass
36 def dropEvent(self, event): pass
36 def dropEvent(self, event): pass
37
37
38
38
39 class ConsoleWidget(Configurable, QtGui.QWidget):
39 class ConsoleWidget(Configurable, QtGui.QWidget):
40 """ An abstract base class for console-type widgets. This class has
40 """ An abstract base class for console-type widgets. This class has
41 functionality for:
41 functionality for:
42
42
43 * Maintaining a prompt and editing region
43 * Maintaining a prompt and editing region
44 * Providing the traditional Unix-style console keyboard shortcuts
44 * Providing the traditional Unix-style console keyboard shortcuts
45 * Performing tab completion
45 * Performing tab completion
46 * Paging text
46 * Paging text
47 * Handling ANSI escape codes
47 * Handling ANSI escape codes
48
48
49 ConsoleWidget also provides a number of utility methods that will be
49 ConsoleWidget also provides a number of utility methods that will be
50 convenient to implementors of a console-style widget.
50 convenient to implementors of a console-style widget.
51 """
51 """
52 __metaclass__ = MetaQObjectHasTraits
52 __metaclass__ = MetaQObjectHasTraits
53
53
54 # Whether to process ANSI escape codes.
54 # Whether to process ANSI escape codes.
55 ansi_codes = Bool(True, config=True)
55 ansi_codes = Bool(True, config=True)
56
56
57 # The maximum number of lines of text before truncation. Specifying a
57 # The maximum number of lines of text before truncation. Specifying a
58 # non-positive number disables text truncation (not recommended).
58 # non-positive number disables text truncation (not recommended).
59 buffer_size = Int(500, config=True)
59 buffer_size = Int(500, config=True)
60
60
61 # Whether to use a list widget or plain text output for tab completion.
61 # Whether to use a list widget or plain text output for tab completion.
62 gui_completion = Bool(False, config=True)
62 gui_completion = Bool(False, config=True)
63
63
64 # The type of underlying text widget to use. Valid values are 'plain', which
64 # The type of underlying text widget to use. Valid values are 'plain', which
65 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
65 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
66 # NOTE: this value can only be specified during initialization.
66 # NOTE: this value can only be specified during initialization.
67 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
67 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
68
68
69 # The type of paging to use. Valid values are:
69 # The type of paging to use. Valid values are:
70 # 'inside' : The widget pages like a traditional terminal pager.
70 # 'inside' : The widget pages like a traditional terminal pager.
71 # 'hsplit' : When paging is requested, the widget is split
71 # 'hsplit' : When paging is requested, the widget is split
72 # horizontally. The top pane contains the console, and the
72 # horizontally. The top pane contains the console, and the
73 # bottom pane contains the paged text.
73 # bottom pane contains the paged text.
74 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
74 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
75 # 'custom' : No action is taken by the widget beyond emitting a
75 # 'custom' : No action is taken by the widget beyond emitting a
76 # 'custom_page_requested(str)' signal.
76 # 'custom_page_requested(str)' signal.
77 # 'none' : The text is written directly to the console.
77 # 'none' : The text is written directly to the console.
78 # NOTE: this value can only be specified during initialization.
78 # NOTE: this value can only be specified during initialization.
79 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
79 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
80 default_value='inside', config=True)
80 default_value='inside', config=True)
81
81
82 # Whether to override ShortcutEvents for the keybindings defined by this
82 # Whether to override ShortcutEvents for the keybindings defined by this
83 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
83 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
84 # priority (when it has focus) over, e.g., window-level menu shortcuts.
84 # priority (when it has focus) over, e.g., window-level menu shortcuts.
85 override_shortcuts = Bool(False)
85 override_shortcuts = Bool(False)
86
86
87 # Signals that indicate ConsoleWidget state.
87 # Signals that indicate ConsoleWidget state.
88 copy_available = QtCore.pyqtSignal(bool)
88 copy_available = QtCore.pyqtSignal(bool)
89 redo_available = QtCore.pyqtSignal(bool)
89 redo_available = QtCore.pyqtSignal(bool)
90 undo_available = QtCore.pyqtSignal(bool)
90 undo_available = QtCore.pyqtSignal(bool)
91
91
92 # Signal emitted when paging is needed and the paging style has been
92 # Signal emitted when paging is needed and the paging style has been
93 # specified as 'custom'.
93 # specified as 'custom'.
94 custom_page_requested = QtCore.pyqtSignal(object)
94 custom_page_requested = QtCore.pyqtSignal(object)
95
95
96 # Protected class variables.
96 # Protected class variables.
97 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
97 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
98 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
98 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
99 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
99 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
100 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
100 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
101 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
101 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
102 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
102 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
103 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
103 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
104 _shortcuts = set(_ctrl_down_remap.keys() +
104 _shortcuts = set(_ctrl_down_remap.keys() +
105 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
105 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
106 QtCore.Qt.Key_V ])
106 QtCore.Qt.Key_V ])
107
107
108 #---------------------------------------------------------------------------
108 #---------------------------------------------------------------------------
109 # 'QObject' interface
109 # 'QObject' interface
110 #---------------------------------------------------------------------------
110 #---------------------------------------------------------------------------
111
111
112 def __init__(self, parent=None, **kw):
112 def __init__(self, parent=None, **kw):
113 """ Create a ConsoleWidget.
113 """ Create a ConsoleWidget.
114
114
115 Parameters:
115 Parameters:
116 -----------
116 -----------
117 parent : QWidget, optional [default None]
117 parent : QWidget, optional [default None]
118 The parent for this widget.
118 The parent for this widget.
119 """
119 """
120 QtGui.QWidget.__init__(self, parent)
120 QtGui.QWidget.__init__(self, parent)
121 Configurable.__init__(self, **kw)
121 Configurable.__init__(self, **kw)
122
122
123 # Create the layout and underlying text widget.
123 # Create the layout and underlying text widget.
124 layout = QtGui.QStackedLayout(self)
124 layout = QtGui.QStackedLayout(self)
125 layout.setContentsMargins(0, 0, 0, 0)
125 layout.setContentsMargins(0, 0, 0, 0)
126 self._control = self._create_control()
126 self._control = self._create_control()
127 self._page_control = None
127 self._page_control = None
128 self._splitter = None
128 self._splitter = None
129 if self.paging in ('hsplit', 'vsplit'):
129 if self.paging in ('hsplit', 'vsplit'):
130 self._splitter = QtGui.QSplitter()
130 self._splitter = QtGui.QSplitter()
131 if self.paging == 'hsplit':
131 if self.paging == 'hsplit':
132 self._splitter.setOrientation(QtCore.Qt.Horizontal)
132 self._splitter.setOrientation(QtCore.Qt.Horizontal)
133 else:
133 else:
134 self._splitter.setOrientation(QtCore.Qt.Vertical)
134 self._splitter.setOrientation(QtCore.Qt.Vertical)
135 self._splitter.addWidget(self._control)
135 self._splitter.addWidget(self._control)
136 layout.addWidget(self._splitter)
136 layout.addWidget(self._splitter)
137 else:
137 else:
138 layout.addWidget(self._control)
138 layout.addWidget(self._control)
139
139
140 # Create the paging widget, if necessary.
140 # Create the paging widget, if necessary.
141 if self.paging in ('inside', 'hsplit', 'vsplit'):
141 if self.paging in ('inside', 'hsplit', 'vsplit'):
142 self._page_control = self._create_page_control()
142 self._page_control = self._create_page_control()
143 if self._splitter:
143 if self._splitter:
144 self._page_control.hide()
144 self._page_control.hide()
145 self._splitter.addWidget(self._page_control)
145 self._splitter.addWidget(self._page_control)
146 else:
146 else:
147 layout.addWidget(self._page_control)
147 layout.addWidget(self._page_control)
148
148
149 # Initialize protected variables. Some variables contain useful state
149 # Initialize protected variables. Some variables contain useful state
150 # information for subclasses; they should be considered read-only.
150 # information for subclasses; they should be considered read-only.
151 self._ansi_processor = QtAnsiCodeProcessor()
151 self._ansi_processor = QtAnsiCodeProcessor()
152 self._completion_widget = CompletionWidget(self._control)
152 self._completion_widget = CompletionWidget(self._control)
153 self._continuation_prompt = '> '
153 self._continuation_prompt = '> '
154 self._continuation_prompt_html = None
154 self._continuation_prompt_html = None
155 self._executing = False
155 self._executing = False
156 self._prompt = ''
156 self._prompt = ''
157 self._prompt_html = None
157 self._prompt_html = None
158 self._prompt_pos = 0
158 self._prompt_pos = 0
159 self._prompt_sep = ''
159 self._prompt_sep = ''
160 self._reading = False
160 self._reading = False
161 self._reading_callback = None
161 self._reading_callback = None
162 self._tab_width = 8
162 self._tab_width = 8
163 self._text_completing_pos = 0
163 self._text_completing_pos = 0
164
164
165 # Set a monospaced font.
165 # Set a monospaced font.
166 self.reset_font()
166 self.reset_font()
167
167
168 def eventFilter(self, obj, event):
168 def eventFilter(self, obj, event):
169 """ Reimplemented to ensure a console-like behavior in the underlying
169 """ Reimplemented to ensure a console-like behavior in the underlying
170 text widgets.
170 text widgets.
171 """
171 """
172 etype = event.type()
172 etype = event.type()
173 if etype == QtCore.QEvent.KeyPress:
173 if etype == QtCore.QEvent.KeyPress:
174
174
175 # Re-map keys for all filtered widgets.
175 # Re-map keys for all filtered widgets.
176 key = event.key()
176 key = event.key()
177 if self._control_key_down(event.modifiers()) and \
177 if self._control_key_down(event.modifiers()) and \
178 key in self._ctrl_down_remap:
178 key in self._ctrl_down_remap:
179 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
179 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
180 self._ctrl_down_remap[key],
180 self._ctrl_down_remap[key],
181 QtCore.Qt.NoModifier)
181 QtCore.Qt.NoModifier)
182 QtGui.qApp.sendEvent(obj, new_event)
182 QtGui.qApp.sendEvent(obj, new_event)
183 return True
183 return True
184
184
185 elif obj == self._control:
185 elif obj == self._control:
186 return self._event_filter_console_keypress(event)
186 return self._event_filter_console_keypress(event)
187
187
188 elif obj == self._page_control:
188 elif obj == self._page_control:
189 return self._event_filter_page_keypress(event)
189 return self._event_filter_page_keypress(event)
190
190
191 # Make middle-click paste safe.
191 # Make middle-click paste safe.
192 elif etype == QtCore.QEvent.MouseButtonRelease and \
192 elif etype == QtCore.QEvent.MouseButtonRelease and \
193 event.button() == QtCore.Qt.MidButton and \
193 event.button() == QtCore.Qt.MidButton and \
194 obj == self._control.viewport():
194 obj == self._control.viewport():
195 cursor = self._control.cursorForPosition(event.pos())
195 cursor = self._control.cursorForPosition(event.pos())
196 self._control.setTextCursor(cursor)
196 self._control.setTextCursor(cursor)
197 self.paste(QtGui.QClipboard.Selection)
197 self.paste(QtGui.QClipboard.Selection)
198 return True
198 return True
199
199
200 # Override shortcuts for all filtered widgets.
200 # Override shortcuts for all filtered widgets.
201 elif etype == QtCore.QEvent.ShortcutOverride and \
201 elif etype == QtCore.QEvent.ShortcutOverride and \
202 self.override_shortcuts and \
202 self._control_key_down(event.modifiers()) and \
203 self._control_key_down(event.modifiers()) and \
203 event.key() in self._shortcuts:
204 event.key() in self._shortcuts:
204 event.accept()
205 event.accept()
205 return False
206 return False
206
207
207 return super(ConsoleWidget, self).eventFilter(obj, event)
208 return super(ConsoleWidget, self).eventFilter(obj, event)
208
209
209 #---------------------------------------------------------------------------
210 #---------------------------------------------------------------------------
210 # 'QWidget' interface
211 # 'QWidget' interface
211 #---------------------------------------------------------------------------
212 #---------------------------------------------------------------------------
212
213
213 def sizeHint(self):
214 def sizeHint(self):
214 """ Reimplemented to suggest a size that is 80 characters wide and
215 """ Reimplemented to suggest a size that is 80 characters wide and
215 25 lines high.
216 25 lines high.
216 """
217 """
217 font_metrics = QtGui.QFontMetrics(self.font)
218 font_metrics = QtGui.QFontMetrics(self.font)
218 margin = (self._control.frameWidth() +
219 margin = (self._control.frameWidth() +
219 self._control.document().documentMargin()) * 2
220 self._control.document().documentMargin()) * 2
220 style = self.style()
221 style = self.style()
221 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
222 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
222
223
223 # Note 1: Despite my best efforts to take the various margins into
224 # Note 1: Despite my best efforts to take the various margins into
224 # account, the width is still coming out a bit too small, so we include
225 # account, the width is still coming out a bit too small, so we include
225 # a fudge factor of one character here.
226 # a fudge factor of one character here.
226 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
227 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
227 # to a Qt bug on certain Mac OS systems where it returns 0.
228 # to a Qt bug on certain Mac OS systems where it returns 0.
228 width = font_metrics.width(' ') * 81 + margin
229 width = font_metrics.width(' ') * 81 + margin
229 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
230 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
230 if self.paging == 'hsplit':
231 if self.paging == 'hsplit':
231 width = width * 2 + splitwidth
232 width = width * 2 + splitwidth
232
233
233 height = font_metrics.height() * 25 + margin
234 height = font_metrics.height() * 25 + margin
234 if self.paging == 'vsplit':
235 if self.paging == 'vsplit':
235 height = height * 2 + splitwidth
236 height = height * 2 + splitwidth
236
237
237 return QtCore.QSize(width, height)
238 return QtCore.QSize(width, height)
238
239
239 #---------------------------------------------------------------------------
240 #---------------------------------------------------------------------------
240 # 'ConsoleWidget' public interface
241 # 'ConsoleWidget' public interface
241 #---------------------------------------------------------------------------
242 #---------------------------------------------------------------------------
242
243
243 def can_paste(self):
244 def can_paste(self):
244 """ Returns whether text can be pasted from the clipboard.
245 """ Returns whether text can be pasted from the clipboard.
245 """
246 """
246 # Only accept text that can be ASCII encoded.
247 # Only accept text that can be ASCII encoded.
247 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
248 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
248 text = QtGui.QApplication.clipboard().text()
249 text = QtGui.QApplication.clipboard().text()
249 if not text.isEmpty():
250 if not text.isEmpty():
250 try:
251 try:
251 str(text)
252 str(text)
252 return True
253 return True
253 except UnicodeEncodeError:
254 except UnicodeEncodeError:
254 pass
255 pass
255 return False
256 return False
256
257
257 def clear(self, keep_input=True):
258 def clear(self, keep_input=True):
258 """ Clear the console, then write a new prompt. If 'keep_input' is set,
259 """ Clear the console, then write a new prompt. If 'keep_input' is set,
259 restores the old input buffer when the new prompt is written.
260 restores the old input buffer when the new prompt is written.
260 """
261 """
261 if keep_input:
262 if keep_input:
262 input_buffer = self.input_buffer
263 input_buffer = self.input_buffer
263 self._control.clear()
264 self._control.clear()
264 self._show_prompt()
265 self._show_prompt()
265 if keep_input:
266 if keep_input:
266 self.input_buffer = input_buffer
267 self.input_buffer = input_buffer
267
268
268 def copy(self):
269 def copy(self):
269 """ Copy the current selected text to the clipboard.
270 """ Copy the current selected text to the clipboard.
270 """
271 """
271 self._control.copy()
272 self._control.copy()
272
273
273 def execute(self, source=None, hidden=False, interactive=False):
274 def execute(self, source=None, hidden=False, interactive=False):
274 """ Executes source or the input buffer, possibly prompting for more
275 """ Executes source or the input buffer, possibly prompting for more
275 input.
276 input.
276
277
277 Parameters:
278 Parameters:
278 -----------
279 -----------
279 source : str, optional
280 source : str, optional
280
281
281 The source to execute. If not specified, the input buffer will be
282 The source to execute. If not specified, the input buffer will be
282 used. If specified and 'hidden' is False, the input buffer will be
283 used. If specified and 'hidden' is False, the input buffer will be
283 replaced with the source before execution.
284 replaced with the source before execution.
284
285
285 hidden : bool, optional (default False)
286 hidden : bool, optional (default False)
286
287
287 If set, no output will be shown and the prompt will not be modified.
288 If set, no output will be shown and the prompt will not be modified.
288 In other words, it will be completely invisible to the user that
289 In other words, it will be completely invisible to the user that
289 an execution has occurred.
290 an execution has occurred.
290
291
291 interactive : bool, optional (default False)
292 interactive : bool, optional (default False)
292
293
293 Whether the console is to treat the source as having been manually
294 Whether the console is to treat the source as having been manually
294 entered by the user. The effect of this parameter depends on the
295 entered by the user. The effect of this parameter depends on the
295 subclass implementation.
296 subclass implementation.
296
297
297 Raises:
298 Raises:
298 -------
299 -------
299 RuntimeError
300 RuntimeError
300 If incomplete input is given and 'hidden' is True. In this case,
301 If incomplete input is given and 'hidden' is True. In this case,
301 it is not possible to prompt for more input.
302 it is not possible to prompt for more input.
302
303
303 Returns:
304 Returns:
304 --------
305 --------
305 A boolean indicating whether the source was executed.
306 A boolean indicating whether the source was executed.
306 """
307 """
307 # WARNING: The order in which things happen here is very particular, in
308 # WARNING: The order in which things happen here is very particular, in
308 # large part because our syntax highlighting is fragile. If you change
309 # large part because our syntax highlighting is fragile. If you change
309 # something, test carefully!
310 # something, test carefully!
310
311
311 # Decide what to execute.
312 # Decide what to execute.
312 if source is None:
313 if source is None:
313 source = self.input_buffer
314 source = self.input_buffer
314 if not hidden:
315 if not hidden:
315 # A newline is appended later, but it should be considered part
316 # A newline is appended later, but it should be considered part
316 # of the input buffer.
317 # of the input buffer.
317 source += '\n'
318 source += '\n'
318 elif not hidden:
319 elif not hidden:
319 self.input_buffer = source
320 self.input_buffer = source
320
321
321 # Execute the source or show a continuation prompt if it is incomplete.
322 # Execute the source or show a continuation prompt if it is incomplete.
322 complete = self._is_complete(source, interactive)
323 complete = self._is_complete(source, interactive)
323 if hidden:
324 if hidden:
324 if complete:
325 if complete:
325 self._execute(source, hidden)
326 self._execute(source, hidden)
326 else:
327 else:
327 error = 'Incomplete noninteractive input: "%s"'
328 error = 'Incomplete noninteractive input: "%s"'
328 raise RuntimeError(error % source)
329 raise RuntimeError(error % source)
329 else:
330 else:
330 if complete:
331 if complete:
331 self._append_plain_text('\n')
332 self._append_plain_text('\n')
332 self._executing_input_buffer = self.input_buffer
333 self._executing_input_buffer = self.input_buffer
333 self._executing = True
334 self._executing = True
334 self._prompt_finished()
335 self._prompt_finished()
335
336
336 # The maximum block count is only in effect during execution.
337 # The maximum block count is only in effect during execution.
337 # This ensures that _prompt_pos does not become invalid due to
338 # This ensures that _prompt_pos does not become invalid due to
338 # text truncation.
339 # text truncation.
339 self._control.document().setMaximumBlockCount(self.buffer_size)
340 self._control.document().setMaximumBlockCount(self.buffer_size)
340
341
341 # Setting a positive maximum block count will automatically
342 # Setting a positive maximum block count will automatically
342 # disable the undo/redo history, but just to be safe:
343 # disable the undo/redo history, but just to be safe:
343 self._control.setUndoRedoEnabled(False)
344 self._control.setUndoRedoEnabled(False)
344
345
345 self._execute(source, hidden)
346 self._execute(source, hidden)
346
347
347 else:
348 else:
348 # Do this inside an edit block so continuation prompts are
349 # Do this inside an edit block so continuation prompts are
349 # removed seamlessly via undo/redo.
350 # removed seamlessly via undo/redo.
350 cursor = self._get_end_cursor()
351 cursor = self._get_end_cursor()
351 cursor.beginEditBlock()
352 cursor.beginEditBlock()
352 cursor.insertText('\n')
353 cursor.insertText('\n')
353 self._insert_continuation_prompt(cursor)
354 self._insert_continuation_prompt(cursor)
354 cursor.endEditBlock()
355 cursor.endEditBlock()
355
356
356 # Do not do this inside the edit block. It works as expected
357 # Do not do this inside the edit block. It works as expected
357 # when using a QPlainTextEdit control, but does not have an
358 # when using a QPlainTextEdit control, but does not have an
358 # effect when using a QTextEdit. I believe this is a Qt bug.
359 # effect when using a QTextEdit. I believe this is a Qt bug.
359 self._control.moveCursor(QtGui.QTextCursor.End)
360 self._control.moveCursor(QtGui.QTextCursor.End)
360
361
361 return complete
362 return complete
362
363
363 def _get_input_buffer(self):
364 def _get_input_buffer(self):
364 """ The text that the user has entered entered at the current prompt.
365 """ The text that the user has entered entered at the current prompt.
365 """
366 """
366 # If we're executing, the input buffer may not even exist anymore due to
367 # If we're executing, the input buffer may not even exist anymore due to
367 # the limit imposed by 'buffer_size'. Therefore, we store it.
368 # the limit imposed by 'buffer_size'. Therefore, we store it.
368 if self._executing:
369 if self._executing:
369 return self._executing_input_buffer
370 return self._executing_input_buffer
370
371
371 cursor = self._get_end_cursor()
372 cursor = self._get_end_cursor()
372 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
373 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
373 input_buffer = str(cursor.selection().toPlainText())
374 input_buffer = str(cursor.selection().toPlainText())
374
375
375 # Strip out continuation prompts.
376 # Strip out continuation prompts.
376 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
377 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
377
378
378 def _set_input_buffer(self, string):
379 def _set_input_buffer(self, string):
379 """ Replaces the text in the input buffer with 'string'.
380 """ Replaces the text in the input buffer with 'string'.
380 """
381 """
381 # For now, it is an error to modify the input buffer during execution.
382 # For now, it is an error to modify the input buffer during execution.
382 if self._executing:
383 if self._executing:
383 raise RuntimeError("Cannot change input buffer during execution.")
384 raise RuntimeError("Cannot change input buffer during execution.")
384
385
385 # Remove old text.
386 # Remove old text.
386 cursor = self._get_end_cursor()
387 cursor = self._get_end_cursor()
387 cursor.beginEditBlock()
388 cursor.beginEditBlock()
388 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
389 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
389 cursor.removeSelectedText()
390 cursor.removeSelectedText()
390
391
391 # Insert new text with continuation prompts.
392 # Insert new text with continuation prompts.
392 lines = string.splitlines(True)
393 lines = string.splitlines(True)
393 if lines:
394 if lines:
394 self._append_plain_text(lines[0])
395 self._append_plain_text(lines[0])
395 for i in xrange(1, len(lines)):
396 for i in xrange(1, len(lines)):
396 if self._continuation_prompt_html is None:
397 if self._continuation_prompt_html is None:
397 self._append_plain_text(self._continuation_prompt)
398 self._append_plain_text(self._continuation_prompt)
398 else:
399 else:
399 self._append_html(self._continuation_prompt_html)
400 self._append_html(self._continuation_prompt_html)
400 self._append_plain_text(lines[i])
401 self._append_plain_text(lines[i])
401 cursor.endEditBlock()
402 cursor.endEditBlock()
402 self._control.moveCursor(QtGui.QTextCursor.End)
403 self._control.moveCursor(QtGui.QTextCursor.End)
403
404
404 input_buffer = property(_get_input_buffer, _set_input_buffer)
405 input_buffer = property(_get_input_buffer, _set_input_buffer)
405
406
406 def _get_font(self):
407 def _get_font(self):
407 """ The base font being used by the ConsoleWidget.
408 """ The base font being used by the ConsoleWidget.
408 """
409 """
409 return self._control.document().defaultFont()
410 return self._control.document().defaultFont()
410
411
411 def _set_font(self, font):
412 def _set_font(self, font):
412 """ Sets the base font for the ConsoleWidget to the specified QFont.
413 """ Sets the base font for the ConsoleWidget to the specified QFont.
413 """
414 """
414 font_metrics = QtGui.QFontMetrics(font)
415 font_metrics = QtGui.QFontMetrics(font)
415 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
416 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
416
417
417 self._completion_widget.setFont(font)
418 self._completion_widget.setFont(font)
418 self._control.document().setDefaultFont(font)
419 self._control.document().setDefaultFont(font)
419 if self._page_control:
420 if self._page_control:
420 self._page_control.document().setDefaultFont(font)
421 self._page_control.document().setDefaultFont(font)
421
422
422 font = property(_get_font, _set_font)
423 font = property(_get_font, _set_font)
423
424
424 def paste(self, mode=QtGui.QClipboard.Clipboard):
425 def paste(self, mode=QtGui.QClipboard.Clipboard):
425 """ Paste the contents of the clipboard into the input region.
426 """ Paste the contents of the clipboard into the input region.
426
427
427 Parameters:
428 Parameters:
428 -----------
429 -----------
429 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
430 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
430
431
431 Controls which part of the system clipboard is used. This can be
432 Controls which part of the system clipboard is used. This can be
432 used to access the selection clipboard in X11 and the Find buffer
433 used to access the selection clipboard in X11 and the Find buffer
433 in Mac OS. By default, the regular clipboard is used.
434 in Mac OS. By default, the regular clipboard is used.
434 """
435 """
435 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
436 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
436 try:
437 try:
437 text = str(QtGui.QApplication.clipboard().text(mode))
438 text = str(QtGui.QApplication.clipboard().text(mode))
438 except UnicodeEncodeError:
439 except UnicodeEncodeError:
439 pass
440 pass
440 else:
441 else:
441 self._insert_plain_text_into_buffer(dedent(text))
442 self._insert_plain_text_into_buffer(dedent(text))
442
443
443 def print_(self, printer):
444 def print_(self, printer):
444 """ Print the contents of the ConsoleWidget to the specified QPrinter.
445 """ Print the contents of the ConsoleWidget to the specified QPrinter.
445 """
446 """
446 self._control.print_(printer)
447 self._control.print_(printer)
447
448
448 def redo(self):
449 def redo(self):
449 """ Redo the last operation. If there is no operation to redo, nothing
450 """ Redo the last operation. If there is no operation to redo, nothing
450 happens.
451 happens.
451 """
452 """
452 self._control.redo()
453 self._control.redo()
453
454
454 def reset_font(self):
455 def reset_font(self):
455 """ Sets the font to the default fixed-width font for this platform.
456 """ Sets the font to the default fixed-width font for this platform.
456 """
457 """
457 if sys.platform == 'win32':
458 if sys.platform == 'win32':
458 # FIXME: we should test whether Consolas is available and use it
459 # FIXME: we should test whether Consolas is available and use it
459 # first if it is. Consolas ships by default from Vista onwards,
460 # first if it is. Consolas ships by default from Vista onwards,
460 # it's *vastly* more readable and prettier than Courier, and is
461 # it's *vastly* more readable and prettier than Courier, and is
461 # often installed even on XP systems. So we should first check for
462 # often installed even on XP systems. So we should first check for
462 # it, and only fallback to Courier if absolutely necessary.
463 # it, and only fallback to Courier if absolutely necessary.
463 name = 'Courier'
464 name = 'Courier'
464 elif sys.platform == 'darwin':
465 elif sys.platform == 'darwin':
465 name = 'Monaco'
466 name = 'Monaco'
466 else:
467 else:
467 name = 'Monospace'
468 name = 'Monospace'
468 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
469 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
469 font.setStyleHint(QtGui.QFont.TypeWriter)
470 font.setStyleHint(QtGui.QFont.TypeWriter)
470 self._set_font(font)
471 self._set_font(font)
471
472
472 def select_all(self):
473 def select_all(self):
473 """ Selects all the text in the buffer.
474 """ Selects all the text in the buffer.
474 """
475 """
475 self._control.selectAll()
476 self._control.selectAll()
476
477
477 def _get_tab_width(self):
478 def _get_tab_width(self):
478 """ The width (in terms of space characters) for tab characters.
479 """ The width (in terms of space characters) for tab characters.
479 """
480 """
480 return self._tab_width
481 return self._tab_width
481
482
482 def _set_tab_width(self, tab_width):
483 def _set_tab_width(self, tab_width):
483 """ Sets the width (in terms of space characters) for tab characters.
484 """ Sets the width (in terms of space characters) for tab characters.
484 """
485 """
485 font_metrics = QtGui.QFontMetrics(self.font)
486 font_metrics = QtGui.QFontMetrics(self.font)
486 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
487 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
487
488
488 self._tab_width = tab_width
489 self._tab_width = tab_width
489
490
490 tab_width = property(_get_tab_width, _set_tab_width)
491 tab_width = property(_get_tab_width, _set_tab_width)
491
492
492 def undo(self):
493 def undo(self):
493 """ Undo the last operation. If there is no operation to undo, nothing
494 """ Undo the last operation. If there is no operation to undo, nothing
494 happens.
495 happens.
495 """
496 """
496 self._control.undo()
497 self._control.undo()
497
498
498 #---------------------------------------------------------------------------
499 #---------------------------------------------------------------------------
499 # 'ConsoleWidget' abstract interface
500 # 'ConsoleWidget' abstract interface
500 #---------------------------------------------------------------------------
501 #---------------------------------------------------------------------------
501
502
502 def _is_complete(self, source, interactive):
503 def _is_complete(self, source, interactive):
503 """ Returns whether 'source' can be executed. When triggered by an
504 """ Returns whether 'source' can be executed. When triggered by an
504 Enter/Return key press, 'interactive' is True; otherwise, it is
505 Enter/Return key press, 'interactive' is True; otherwise, it is
505 False.
506 False.
506 """
507 """
507 raise NotImplementedError
508 raise NotImplementedError
508
509
509 def _execute(self, source, hidden):
510 def _execute(self, source, hidden):
510 """ Execute 'source'. If 'hidden', do not show any output.
511 """ Execute 'source'. If 'hidden', do not show any output.
511 """
512 """
512 raise NotImplementedError
513 raise NotImplementedError
513
514
514 def _prompt_started_hook(self):
515 def _prompt_started_hook(self):
515 """ Called immediately after a new prompt is displayed.
516 """ Called immediately after a new prompt is displayed.
516 """
517 """
517 pass
518 pass
518
519
519 def _prompt_finished_hook(self):
520 def _prompt_finished_hook(self):
520 """ Called immediately after a prompt is finished, i.e. when some input
521 """ Called immediately after a prompt is finished, i.e. when some input
521 will be processed and a new prompt displayed.
522 will be processed and a new prompt displayed.
522 """
523 """
523 pass
524 pass
524
525
525 def _up_pressed(self):
526 def _up_pressed(self):
526 """ Called when the up key is pressed. Returns whether to continue
527 """ Called when the up key is pressed. Returns whether to continue
527 processing the event.
528 processing the event.
528 """
529 """
529 return True
530 return True
530
531
531 def _down_pressed(self):
532 def _down_pressed(self):
532 """ Called when the down key is pressed. Returns whether to continue
533 """ Called when the down key is pressed. Returns whether to continue
533 processing the event.
534 processing the event.
534 """
535 """
535 return True
536 return True
536
537
537 def _tab_pressed(self):
538 def _tab_pressed(self):
538 """ Called when the tab key is pressed. Returns whether to continue
539 """ Called when the tab key is pressed. Returns whether to continue
539 processing the event.
540 processing the event.
540 """
541 """
541 return False
542 return False
542
543
543 #--------------------------------------------------------------------------
544 #--------------------------------------------------------------------------
544 # 'ConsoleWidget' protected interface
545 # 'ConsoleWidget' protected interface
545 #--------------------------------------------------------------------------
546 #--------------------------------------------------------------------------
546
547
547 def _append_html(self, html):
548 def _append_html(self, html):
548 """ Appends html at the end of the console buffer.
549 """ Appends html at the end of the console buffer.
549 """
550 """
550 cursor = self._get_end_cursor()
551 cursor = self._get_end_cursor()
551 self._insert_html(cursor, html)
552 self._insert_html(cursor, html)
552
553
553 def _append_html_fetching_plain_text(self, html):
554 def _append_html_fetching_plain_text(self, html):
554 """ Appends 'html', then returns the plain text version of it.
555 """ Appends 'html', then returns the plain text version of it.
555 """
556 """
556 cursor = self._get_end_cursor()
557 cursor = self._get_end_cursor()
557 return self._insert_html_fetching_plain_text(cursor, html)
558 return self._insert_html_fetching_plain_text(cursor, html)
558
559
559 def _append_plain_text(self, text):
560 def _append_plain_text(self, text):
560 """ Appends plain text at the end of the console buffer, processing
561 """ Appends plain text at the end of the console buffer, processing
561 ANSI codes if enabled.
562 ANSI codes if enabled.
562 """
563 """
563 cursor = self._get_end_cursor()
564 cursor = self._get_end_cursor()
564 self._insert_plain_text(cursor, text)
565 self._insert_plain_text(cursor, text)
565
566
566 def _append_plain_text_keeping_prompt(self, text):
567 def _append_plain_text_keeping_prompt(self, text):
567 """ Writes 'text' after the current prompt, then restores the old prompt
568 """ Writes 'text' after the current prompt, then restores the old prompt
568 with its old input buffer.
569 with its old input buffer.
569 """
570 """
570 input_buffer = self.input_buffer
571 input_buffer = self.input_buffer
571 self._append_plain_text('\n')
572 self._append_plain_text('\n')
572 self._prompt_finished()
573 self._prompt_finished()
573
574
574 self._append_plain_text(text)
575 self._append_plain_text(text)
575 self._show_prompt()
576 self._show_prompt()
576 self.input_buffer = input_buffer
577 self.input_buffer = input_buffer
577
578
578 def _cancel_text_completion(self):
579 def _cancel_text_completion(self):
579 """ If text completion is progress, cancel it.
580 """ If text completion is progress, cancel it.
580 """
581 """
581 if self._text_completing_pos:
582 if self._text_completing_pos:
582 self._clear_temporary_buffer()
583 self._clear_temporary_buffer()
583 self._text_completing_pos = 0
584 self._text_completing_pos = 0
584
585
585 def _clear_temporary_buffer(self):
586 def _clear_temporary_buffer(self):
586 """ Clears the "temporary text" buffer, i.e. all the text following
587 """ Clears the "temporary text" buffer, i.e. all the text following
587 the prompt region.
588 the prompt region.
588 """
589 """
589 # Select and remove all text below the input buffer.
590 # Select and remove all text below the input buffer.
590 cursor = self._get_prompt_cursor()
591 cursor = self._get_prompt_cursor()
591 prompt = self._continuation_prompt.lstrip()
592 prompt = self._continuation_prompt.lstrip()
592 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
593 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
593 temp_cursor = QtGui.QTextCursor(cursor)
594 temp_cursor = QtGui.QTextCursor(cursor)
594 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
595 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
595 text = str(temp_cursor.selection().toPlainText()).lstrip()
596 text = str(temp_cursor.selection().toPlainText()).lstrip()
596 if not text.startswith(prompt):
597 if not text.startswith(prompt):
597 break
598 break
598 else:
599 else:
599 # We've reached the end of the input buffer and no text follows.
600 # We've reached the end of the input buffer and no text follows.
600 return
601 return
601 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
602 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
602 cursor.movePosition(QtGui.QTextCursor.End,
603 cursor.movePosition(QtGui.QTextCursor.End,
603 QtGui.QTextCursor.KeepAnchor)
604 QtGui.QTextCursor.KeepAnchor)
604 cursor.removeSelectedText()
605 cursor.removeSelectedText()
605
606
606 # After doing this, we have no choice but to clear the undo/redo
607 # After doing this, we have no choice but to clear the undo/redo
607 # history. Otherwise, the text is not "temporary" at all, because it
608 # history. Otherwise, the text is not "temporary" at all, because it
608 # can be recalled with undo/redo. Unfortunately, Qt does not expose
609 # can be recalled with undo/redo. Unfortunately, Qt does not expose
609 # fine-grained control to the undo/redo system.
610 # fine-grained control to the undo/redo system.
610 if self._control.isUndoRedoEnabled():
611 if self._control.isUndoRedoEnabled():
611 self._control.setUndoRedoEnabled(False)
612 self._control.setUndoRedoEnabled(False)
612 self._control.setUndoRedoEnabled(True)
613 self._control.setUndoRedoEnabled(True)
613
614
614 def _complete_with_items(self, cursor, items):
615 def _complete_with_items(self, cursor, items):
615 """ Performs completion with 'items' at the specified cursor location.
616 """ Performs completion with 'items' at the specified cursor location.
616 """
617 """
617 self._cancel_text_completion()
618 self._cancel_text_completion()
618
619
619 if len(items) == 1:
620 if len(items) == 1:
620 cursor.setPosition(self._control.textCursor().position(),
621 cursor.setPosition(self._control.textCursor().position(),
621 QtGui.QTextCursor.KeepAnchor)
622 QtGui.QTextCursor.KeepAnchor)
622 cursor.insertText(items[0])
623 cursor.insertText(items[0])
623
624
624 elif len(items) > 1:
625 elif len(items) > 1:
625 current_pos = self._control.textCursor().position()
626 current_pos = self._control.textCursor().position()
626 prefix = commonprefix(items)
627 prefix = commonprefix(items)
627 if prefix:
628 if prefix:
628 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
629 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
629 cursor.insertText(prefix)
630 cursor.insertText(prefix)
630 current_pos = cursor.position()
631 current_pos = cursor.position()
631
632
632 if self.gui_completion:
633 if self.gui_completion:
633 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
634 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
634 self._completion_widget.show_items(cursor, items)
635 self._completion_widget.show_items(cursor, items)
635 else:
636 else:
636 cursor.beginEditBlock()
637 cursor.beginEditBlock()
637 self._append_plain_text('\n')
638 self._append_plain_text('\n')
638 self._page(self._format_as_columns(items))
639 self._page(self._format_as_columns(items))
639 cursor.endEditBlock()
640 cursor.endEditBlock()
640
641
641 cursor.setPosition(current_pos)
642 cursor.setPosition(current_pos)
642 self._control.moveCursor(QtGui.QTextCursor.End)
643 self._control.moveCursor(QtGui.QTextCursor.End)
643 self._control.setTextCursor(cursor)
644 self._control.setTextCursor(cursor)
644 self._text_completing_pos = current_pos
645 self._text_completing_pos = current_pos
645
646
646 def _control_key_down(self, modifiers, include_command=True):
647 def _control_key_down(self, modifiers, include_command=True):
647 """ Given a KeyboardModifiers flags object, return whether the Control
648 """ Given a KeyboardModifiers flags object, return whether the Control
648 key is down.
649 key is down.
649
650
650 Parameters:
651 Parameters:
651 -----------
652 -----------
652 include_command : bool, optional (default True)
653 include_command : bool, optional (default True)
653 Whether to treat the Command key as a (mutually exclusive) synonym
654 Whether to treat the Command key as a (mutually exclusive) synonym
654 for Control when in Mac OS.
655 for Control when in Mac OS.
655 """
656 """
656 # Note that on Mac OS, ControlModifier corresponds to the Command key
657 # Note that on Mac OS, ControlModifier corresponds to the Command key
657 # while MetaModifier corresponds to the Control key.
658 # while MetaModifier corresponds to the Control key.
658 if sys.platform == 'darwin':
659 if sys.platform == 'darwin':
659 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
660 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
660 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
661 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
661 else:
662 else:
662 return bool(modifiers & QtCore.Qt.ControlModifier)
663 return bool(modifiers & QtCore.Qt.ControlModifier)
663
664
664 def _create_control(self):
665 def _create_control(self):
665 """ Creates and connects the underlying text widget.
666 """ Creates and connects the underlying text widget.
666 """
667 """
667 # Create the underlying control.
668 # Create the underlying control.
668 if self.kind == 'plain':
669 if self.kind == 'plain':
669 control = ConsolePlainTextEdit()
670 control = ConsolePlainTextEdit()
670 elif self.kind == 'rich':
671 elif self.kind == 'rich':
671 control = ConsoleTextEdit()
672 control = ConsoleTextEdit()
672 control.setAcceptRichText(False)
673 control.setAcceptRichText(False)
673
674
674 # Install event filters. The filter on the viewport is needed for
675 # Install event filters. The filter on the viewport is needed for
675 # mouse events.
676 # mouse events.
676 control.installEventFilter(self)
677 control.installEventFilter(self)
677 control.viewport().installEventFilter(self)
678 control.viewport().installEventFilter(self)
678
679
679 # Connect signals.
680 # Connect signals.
680 control.cursorPositionChanged.connect(self._cursor_position_changed)
681 control.cursorPositionChanged.connect(self._cursor_position_changed)
681 control.customContextMenuRequested.connect(self._show_context_menu)
682 control.customContextMenuRequested.connect(self._show_context_menu)
682 control.copyAvailable.connect(self.copy_available)
683 control.copyAvailable.connect(self.copy_available)
683 control.redoAvailable.connect(self.redo_available)
684 control.redoAvailable.connect(self.redo_available)
684 control.undoAvailable.connect(self.undo_available)
685 control.undoAvailable.connect(self.undo_available)
685
686
686 # Configure the control.
687 # Configure the control.
687 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
688 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
688 control.setReadOnly(True)
689 control.setReadOnly(True)
689 control.setUndoRedoEnabled(False)
690 control.setUndoRedoEnabled(False)
690 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
691 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
691 return control
692 return control
692
693
693 def _create_page_control(self):
694 def _create_page_control(self):
694 """ Creates and connects the underlying paging widget.
695 """ Creates and connects the underlying paging widget.
695 """
696 """
696 control = ConsolePlainTextEdit()
697 control = ConsolePlainTextEdit()
697 control.installEventFilter(self)
698 control.installEventFilter(self)
698 control.setReadOnly(True)
699 control.setReadOnly(True)
699 control.setUndoRedoEnabled(False)
700 control.setUndoRedoEnabled(False)
700 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
701 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
701 return control
702 return control
702
703
703 def _event_filter_console_keypress(self, event):
704 def _event_filter_console_keypress(self, event):
704 """ Filter key events for the underlying text widget to create a
705 """ Filter key events for the underlying text widget to create a
705 console-like interface.
706 console-like interface.
706 """
707 """
707 intercepted = False
708 intercepted = False
708 cursor = self._control.textCursor()
709 cursor = self._control.textCursor()
709 position = cursor.position()
710 position = cursor.position()
710 key = event.key()
711 key = event.key()
711 ctrl_down = self._control_key_down(event.modifiers())
712 ctrl_down = self._control_key_down(event.modifiers())
712 alt_down = event.modifiers() & QtCore.Qt.AltModifier
713 alt_down = event.modifiers() & QtCore.Qt.AltModifier
713 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
714 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
714
715
715 if event.matches(QtGui.QKeySequence.Paste):
716 if event.matches(QtGui.QKeySequence.Paste):
716 # Call our paste instead of the underlying text widget's.
717 # Call our paste instead of the underlying text widget's.
717 self.paste()
718 self.paste()
718 intercepted = True
719 intercepted = True
719
720
720 elif ctrl_down:
721 elif ctrl_down:
721 if key == QtCore.Qt.Key_G:
722 if key == QtCore.Qt.Key_G:
722 self._keyboard_quit()
723 self._keyboard_quit()
723 intercepted = True
724 intercepted = True
724
725
725 elif key == QtCore.Qt.Key_K:
726 elif key == QtCore.Qt.Key_K:
726 if self._in_buffer(position):
727 if self._in_buffer(position):
727 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
728 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
728 QtGui.QTextCursor.KeepAnchor)
729 QtGui.QTextCursor.KeepAnchor)
729 if not cursor.hasSelection():
730 if not cursor.hasSelection():
730 # Line deletion (remove continuation prompt)
731 # Line deletion (remove continuation prompt)
731 cursor.movePosition(QtGui.QTextCursor.NextBlock,
732 cursor.movePosition(QtGui.QTextCursor.NextBlock,
732 QtGui.QTextCursor.KeepAnchor)
733 QtGui.QTextCursor.KeepAnchor)
733 cursor.movePosition(QtGui.QTextCursor.Right,
734 cursor.movePosition(QtGui.QTextCursor.Right,
734 QtGui.QTextCursor.KeepAnchor,
735 QtGui.QTextCursor.KeepAnchor,
735 len(self._continuation_prompt))
736 len(self._continuation_prompt))
736 cursor.removeSelectedText()
737 cursor.removeSelectedText()
737 intercepted = True
738 intercepted = True
738
739
739 elif key == QtCore.Qt.Key_L:
740 elif key == QtCore.Qt.Key_L:
740 # It would be better to simply move the prompt block to the top
741 # It would be better to simply move the prompt block to the top
741 # of the control viewport. QPlainTextEdit has a private method
742 # of the control viewport. QPlainTextEdit has a private method
742 # to do this (setTopBlock), but it cannot be duplicated here
743 # to do this (setTopBlock), but it cannot be duplicated here
743 # because it requires access to the QTextControl that underlies
744 # because it requires access to the QTextControl that underlies
744 # both QPlainTextEdit and QTextEdit. In short, this can only be
745 # both QPlainTextEdit and QTextEdit. In short, this can only be
745 # achieved by appending newlines after the prompt, which is a
746 # achieved by appending newlines after the prompt, which is a
746 # gigantic hack and likely to cause other problems.
747 # gigantic hack and likely to cause other problems.
747 self.clear()
748 self.clear()
748 intercepted = True
749 intercepted = True
749
750
750 elif key == QtCore.Qt.Key_O:
751 elif key == QtCore.Qt.Key_O:
751 if self._page_control and self._page_control.isVisible():
752 if self._page_control and self._page_control.isVisible():
752 self._page_control.setFocus()
753 self._page_control.setFocus()
753 intercept = True
754 intercept = True
754
755
755 elif key == QtCore.Qt.Key_X:
756 elif key == QtCore.Qt.Key_X:
756 # FIXME: Instead of disabling cut completely, only allow it
757 # FIXME: Instead of disabling cut completely, only allow it
757 # when safe.
758 # when safe.
758 intercepted = True
759 intercepted = True
759
760
760 elif key == QtCore.Qt.Key_Y:
761 elif key == QtCore.Qt.Key_Y:
761 self.paste()
762 self.paste()
762 intercepted = True
763 intercepted = True
763
764
764 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
765 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
765 intercepted = True
766 intercepted = True
766
767
767 elif alt_down:
768 elif alt_down:
768 if key == QtCore.Qt.Key_B:
769 if key == QtCore.Qt.Key_B:
769 self._set_cursor(self._get_word_start_cursor(position))
770 self._set_cursor(self._get_word_start_cursor(position))
770 intercepted = True
771 intercepted = True
771
772
772 elif key == QtCore.Qt.Key_F:
773 elif key == QtCore.Qt.Key_F:
773 self._set_cursor(self._get_word_end_cursor(position))
774 self._set_cursor(self._get_word_end_cursor(position))
774 intercepted = True
775 intercepted = True
775
776
776 elif key == QtCore.Qt.Key_Backspace:
777 elif key == QtCore.Qt.Key_Backspace:
777 cursor = self._get_word_start_cursor(position)
778 cursor = self._get_word_start_cursor(position)
778 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
779 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
779 cursor.removeSelectedText()
780 cursor.removeSelectedText()
780 intercepted = True
781 intercepted = True
781
782
782 elif key == QtCore.Qt.Key_D:
783 elif key == QtCore.Qt.Key_D:
783 cursor = self._get_word_end_cursor(position)
784 cursor = self._get_word_end_cursor(position)
784 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
785 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
785 cursor.removeSelectedText()
786 cursor.removeSelectedText()
786 intercepted = True
787 intercepted = True
787
788
788 elif key == QtCore.Qt.Key_Delete:
789 elif key == QtCore.Qt.Key_Delete:
789 intercepted = True
790 intercepted = True
790
791
791 elif key == QtCore.Qt.Key_Greater:
792 elif key == QtCore.Qt.Key_Greater:
792 self._control.moveCursor(QtGui.QTextCursor.End)
793 self._control.moveCursor(QtGui.QTextCursor.End)
793 intercepted = True
794 intercepted = True
794
795
795 elif key == QtCore.Qt.Key_Less:
796 elif key == QtCore.Qt.Key_Less:
796 self._control.setTextCursor(self._get_prompt_cursor())
797 self._control.setTextCursor(self._get_prompt_cursor())
797 intercepted = True
798 intercepted = True
798
799
799 else:
800 else:
800 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
801 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
801 intercepted = True
802 intercepted = True
802
803
803 # Special handling when tab completing in text mode.
804 # Special handling when tab completing in text mode.
804 self._cancel_text_completion()
805 self._cancel_text_completion()
805
806
806 if self._in_buffer(position):
807 if self._in_buffer(position):
807 if self._reading:
808 if self._reading:
808 self._append_plain_text('\n')
809 self._append_plain_text('\n')
809 self._reading = False
810 self._reading = False
810 if self._reading_callback:
811 if self._reading_callback:
811 self._reading_callback()
812 self._reading_callback()
812
813
813 # If there is only whitespace after the cursor, execute.
814 # If there is only whitespace after the cursor, execute.
814 # Otherwise, split the line with a continuation prompt.
815 # Otherwise, split the line with a continuation prompt.
815 elif not self._executing:
816 elif not self._executing:
816 cursor.movePosition(QtGui.QTextCursor.End,
817 cursor.movePosition(QtGui.QTextCursor.End,
817 QtGui.QTextCursor.KeepAnchor)
818 QtGui.QTextCursor.KeepAnchor)
818 at_end = cursor.selectedText().trimmed().isEmpty()
819 at_end = cursor.selectedText().trimmed().isEmpty()
819 if at_end or shift_down:
820 if at_end or shift_down:
820 self.execute(interactive = not shift_down)
821 self.execute(interactive = not shift_down)
821 else:
822 else:
822 # Do this inside an edit block for clean undo/redo.
823 # Do this inside an edit block for clean undo/redo.
823 cursor.beginEditBlock()
824 cursor.beginEditBlock()
824 cursor.setPosition(position)
825 cursor.setPosition(position)
825 cursor.insertText('\n')
826 cursor.insertText('\n')
826 self._insert_continuation_prompt(cursor)
827 self._insert_continuation_prompt(cursor)
827 cursor.endEditBlock()
828 cursor.endEditBlock()
828
829
829 # Ensure that the whole input buffer is visible.
830 # Ensure that the whole input buffer is visible.
830 # FIXME: This will not be usable if the input buffer
831 # FIXME: This will not be usable if the input buffer
831 # is taller than the console widget.
832 # is taller than the console widget.
832 self._control.moveCursor(QtGui.QTextCursor.End)
833 self._control.moveCursor(QtGui.QTextCursor.End)
833 self._control.setTextCursor(cursor)
834 self._control.setTextCursor(cursor)
834
835
835 elif key == QtCore.Qt.Key_Escape:
836 elif key == QtCore.Qt.Key_Escape:
836 self._keyboard_quit()
837 self._keyboard_quit()
837 intercepted = True
838 intercepted = True
838
839
839 elif key == QtCore.Qt.Key_Up:
840 elif key == QtCore.Qt.Key_Up:
840 if self._reading or not self._up_pressed():
841 if self._reading or not self._up_pressed():
841 intercepted = True
842 intercepted = True
842 else:
843 else:
843 prompt_line = self._get_prompt_cursor().blockNumber()
844 prompt_line = self._get_prompt_cursor().blockNumber()
844 intercepted = cursor.blockNumber() <= prompt_line
845 intercepted = cursor.blockNumber() <= prompt_line
845
846
846 elif key == QtCore.Qt.Key_Down:
847 elif key == QtCore.Qt.Key_Down:
847 if self._reading or not self._down_pressed():
848 if self._reading or not self._down_pressed():
848 intercepted = True
849 intercepted = True
849 else:
850 else:
850 end_line = self._get_end_cursor().blockNumber()
851 end_line = self._get_end_cursor().blockNumber()
851 intercepted = cursor.blockNumber() == end_line
852 intercepted = cursor.blockNumber() == end_line
852
853
853 elif key == QtCore.Qt.Key_Tab:
854 elif key == QtCore.Qt.Key_Tab:
854 if not self._reading:
855 if not self._reading:
855 intercepted = not self._tab_pressed()
856 intercepted = not self._tab_pressed()
856
857
857 elif key == QtCore.Qt.Key_Left:
858 elif key == QtCore.Qt.Key_Left:
858 intercepted = not self._in_buffer(position - 1)
859 intercepted = not self._in_buffer(position - 1)
859
860
860 elif key == QtCore.Qt.Key_Home:
861 elif key == QtCore.Qt.Key_Home:
861 start_line = cursor.blockNumber()
862 start_line = cursor.blockNumber()
862 if start_line == self._get_prompt_cursor().blockNumber():
863 if start_line == self._get_prompt_cursor().blockNumber():
863 start_pos = self._prompt_pos
864 start_pos = self._prompt_pos
864 else:
865 else:
865 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
866 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
866 QtGui.QTextCursor.KeepAnchor)
867 QtGui.QTextCursor.KeepAnchor)
867 start_pos = cursor.position()
868 start_pos = cursor.position()
868 start_pos += len(self._continuation_prompt)
869 start_pos += len(self._continuation_prompt)
869 cursor.setPosition(position)
870 cursor.setPosition(position)
870 if shift_down and self._in_buffer(position):
871 if shift_down and self._in_buffer(position):
871 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
872 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
872 else:
873 else:
873 cursor.setPosition(start_pos)
874 cursor.setPosition(start_pos)
874 self._set_cursor(cursor)
875 self._set_cursor(cursor)
875 intercepted = True
876 intercepted = True
876
877
877 elif key == QtCore.Qt.Key_Backspace:
878 elif key == QtCore.Qt.Key_Backspace:
878
879
879 # Line deletion (remove continuation prompt)
880 # Line deletion (remove continuation prompt)
880 line, col = cursor.blockNumber(), cursor.columnNumber()
881 line, col = cursor.blockNumber(), cursor.columnNumber()
881 if not self._reading and \
882 if not self._reading and \
882 col == len(self._continuation_prompt) and \
883 col == len(self._continuation_prompt) and \
883 line > self._get_prompt_cursor().blockNumber():
884 line > self._get_prompt_cursor().blockNumber():
884 cursor.beginEditBlock()
885 cursor.beginEditBlock()
885 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
886 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
886 QtGui.QTextCursor.KeepAnchor)
887 QtGui.QTextCursor.KeepAnchor)
887 cursor.removeSelectedText()
888 cursor.removeSelectedText()
888 cursor.deletePreviousChar()
889 cursor.deletePreviousChar()
889 cursor.endEditBlock()
890 cursor.endEditBlock()
890 intercepted = True
891 intercepted = True
891
892
892 # Regular backwards deletion
893 # Regular backwards deletion
893 else:
894 else:
894 anchor = cursor.anchor()
895 anchor = cursor.anchor()
895 if anchor == position:
896 if anchor == position:
896 intercepted = not self._in_buffer(position - 1)
897 intercepted = not self._in_buffer(position - 1)
897 else:
898 else:
898 intercepted = not self._in_buffer(min(anchor, position))
899 intercepted = not self._in_buffer(min(anchor, position))
899
900
900 elif key == QtCore.Qt.Key_Delete:
901 elif key == QtCore.Qt.Key_Delete:
901
902
902 # Line deletion (remove continuation prompt)
903 # Line deletion (remove continuation prompt)
903 if not self._reading and self._in_buffer(position) and \
904 if not self._reading and self._in_buffer(position) and \
904 cursor.atBlockEnd() and not cursor.hasSelection():
905 cursor.atBlockEnd() and not cursor.hasSelection():
905 cursor.movePosition(QtGui.QTextCursor.NextBlock,
906 cursor.movePosition(QtGui.QTextCursor.NextBlock,
906 QtGui.QTextCursor.KeepAnchor)
907 QtGui.QTextCursor.KeepAnchor)
907 cursor.movePosition(QtGui.QTextCursor.Right,
908 cursor.movePosition(QtGui.QTextCursor.Right,
908 QtGui.QTextCursor.KeepAnchor,
909 QtGui.QTextCursor.KeepAnchor,
909 len(self._continuation_prompt))
910 len(self._continuation_prompt))
910 cursor.removeSelectedText()
911 cursor.removeSelectedText()
911 intercepted = True
912 intercepted = True
912
913
913 # Regular forwards deletion:
914 # Regular forwards deletion:
914 else:
915 else:
915 anchor = cursor.anchor()
916 anchor = cursor.anchor()
916 intercepted = (not self._in_buffer(anchor) or
917 intercepted = (not self._in_buffer(anchor) or
917 not self._in_buffer(position))
918 not self._in_buffer(position))
918
919
919 # Don't move the cursor if control is down to allow copy-paste using
920 # Don't move the cursor if control is down to allow copy-paste using
920 # the keyboard in any part of the buffer.
921 # the keyboard in any part of the buffer.
921 if not ctrl_down:
922 if not ctrl_down:
922 self._keep_cursor_in_buffer()
923 self._keep_cursor_in_buffer()
923
924
924 return intercepted
925 return intercepted
925
926
926 def _event_filter_page_keypress(self, event):
927 def _event_filter_page_keypress(self, event):
927 """ Filter key events for the paging widget to create console-like
928 """ Filter key events for the paging widget to create console-like
928 interface.
929 interface.
929 """
930 """
930 key = event.key()
931 key = event.key()
931 ctrl_down = self._control_key_down(event.modifiers())
932 ctrl_down = self._control_key_down(event.modifiers())
932 alt_down = event.modifiers() & QtCore.Qt.AltModifier
933 alt_down = event.modifiers() & QtCore.Qt.AltModifier
933
934
934 if ctrl_down:
935 if ctrl_down:
935 if key == QtCore.Qt.Key_O:
936 if key == QtCore.Qt.Key_O:
936 self._control.setFocus()
937 self._control.setFocus()
937 intercept = True
938 intercept = True
938
939
939 elif alt_down:
940 elif alt_down:
940 if key == QtCore.Qt.Key_Greater:
941 if key == QtCore.Qt.Key_Greater:
941 self._page_control.moveCursor(QtGui.QTextCursor.End)
942 self._page_control.moveCursor(QtGui.QTextCursor.End)
942 intercepted = True
943 intercepted = True
943
944
944 elif key == QtCore.Qt.Key_Less:
945 elif key == QtCore.Qt.Key_Less:
945 self._page_control.moveCursor(QtGui.QTextCursor.Start)
946 self._page_control.moveCursor(QtGui.QTextCursor.Start)
946 intercepted = True
947 intercepted = True
947
948
948 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
949 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
949 if self._splitter:
950 if self._splitter:
950 self._page_control.hide()
951 self._page_control.hide()
951 else:
952 else:
952 self.layout().setCurrentWidget(self._control)
953 self.layout().setCurrentWidget(self._control)
953 return True
954 return True
954
955
955 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
956 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
956 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
957 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
957 QtCore.Qt.Key_PageDown,
958 QtCore.Qt.Key_PageDown,
958 QtCore.Qt.NoModifier)
959 QtCore.Qt.NoModifier)
959 QtGui.qApp.sendEvent(self._page_control, new_event)
960 QtGui.qApp.sendEvent(self._page_control, new_event)
960 return True
961 return True
961
962
962 elif key == QtCore.Qt.Key_Backspace:
963 elif key == QtCore.Qt.Key_Backspace:
963 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
964 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
964 QtCore.Qt.Key_PageUp,
965 QtCore.Qt.Key_PageUp,
965 QtCore.Qt.NoModifier)
966 QtCore.Qt.NoModifier)
966 QtGui.qApp.sendEvent(self._page_control, new_event)
967 QtGui.qApp.sendEvent(self._page_control, new_event)
967 return True
968 return True
968
969
969 return False
970 return False
970
971
971 def _format_as_columns(self, items, separator=' '):
972 def _format_as_columns(self, items, separator=' '):
972 """ Transform a list of strings into a single string with columns.
973 """ Transform a list of strings into a single string with columns.
973
974
974 Parameters
975 Parameters
975 ----------
976 ----------
976 items : sequence of strings
977 items : sequence of strings
977 The strings to process.
978 The strings to process.
978
979
979 separator : str, optional [default is two spaces]
980 separator : str, optional [default is two spaces]
980 The string that separates columns.
981 The string that separates columns.
981
982
982 Returns
983 Returns
983 -------
984 -------
984 The formatted string.
985 The formatted string.
985 """
986 """
986 # Note: this code is adapted from columnize 0.3.2.
987 # Note: this code is adapted from columnize 0.3.2.
987 # See http://code.google.com/p/pycolumnize/
988 # See http://code.google.com/p/pycolumnize/
988
989
989 # Calculate the number of characters available.
990 # Calculate the number of characters available.
990 width = self._control.viewport().width()
991 width = self._control.viewport().width()
991 char_width = QtGui.QFontMetrics(self.font).width(' ')
992 char_width = QtGui.QFontMetrics(self.font).width(' ')
992 displaywidth = max(10, (width / char_width) - 1)
993 displaywidth = max(10, (width / char_width) - 1)
993
994
994 # Some degenerate cases.
995 # Some degenerate cases.
995 size = len(items)
996 size = len(items)
996 if size == 0:
997 if size == 0:
997 return '\n'
998 return '\n'
998 elif size == 1:
999 elif size == 1:
999 return '%s\n' % str(items[0])
1000 return '%s\n' % str(items[0])
1000
1001
1001 # Try every row count from 1 upwards
1002 # Try every row count from 1 upwards
1002 array_index = lambda nrows, row, col: nrows*col + row
1003 array_index = lambda nrows, row, col: nrows*col + row
1003 for nrows in range(1, size):
1004 for nrows in range(1, size):
1004 ncols = (size + nrows - 1) // nrows
1005 ncols = (size + nrows - 1) // nrows
1005 colwidths = []
1006 colwidths = []
1006 totwidth = -len(separator)
1007 totwidth = -len(separator)
1007 for col in range(ncols):
1008 for col in range(ncols):
1008 # Get max column width for this column
1009 # Get max column width for this column
1009 colwidth = 0
1010 colwidth = 0
1010 for row in range(nrows):
1011 for row in range(nrows):
1011 i = array_index(nrows, row, col)
1012 i = array_index(nrows, row, col)
1012 if i >= size: break
1013 if i >= size: break
1013 x = items[i]
1014 x = items[i]
1014 colwidth = max(colwidth, len(x))
1015 colwidth = max(colwidth, len(x))
1015 colwidths.append(colwidth)
1016 colwidths.append(colwidth)
1016 totwidth += colwidth + len(separator)
1017 totwidth += colwidth + len(separator)
1017 if totwidth > displaywidth:
1018 if totwidth > displaywidth:
1018 break
1019 break
1019 if totwidth <= displaywidth:
1020 if totwidth <= displaywidth:
1020 break
1021 break
1021
1022
1022 # The smallest number of rows computed and the max widths for each
1023 # The smallest number of rows computed and the max widths for each
1023 # column has been obtained. Now we just have to format each of the rows.
1024 # column has been obtained. Now we just have to format each of the rows.
1024 string = ''
1025 string = ''
1025 for row in range(nrows):
1026 for row in range(nrows):
1026 texts = []
1027 texts = []
1027 for col in range(ncols):
1028 for col in range(ncols):
1028 i = row + nrows*col
1029 i = row + nrows*col
1029 if i >= size:
1030 if i >= size:
1030 texts.append('')
1031 texts.append('')
1031 else:
1032 else:
1032 texts.append(items[i])
1033 texts.append(items[i])
1033 while texts and not texts[-1]:
1034 while texts and not texts[-1]:
1034 del texts[-1]
1035 del texts[-1]
1035 for col in range(len(texts)):
1036 for col in range(len(texts)):
1036 texts[col] = texts[col].ljust(colwidths[col])
1037 texts[col] = texts[col].ljust(colwidths[col])
1037 string += '%s\n' % str(separator.join(texts))
1038 string += '%s\n' % str(separator.join(texts))
1038 return string
1039 return string
1039
1040
1040 def _get_block_plain_text(self, block):
1041 def _get_block_plain_text(self, block):
1041 """ Given a QTextBlock, return its unformatted text.
1042 """ Given a QTextBlock, return its unformatted text.
1042 """
1043 """
1043 cursor = QtGui.QTextCursor(block)
1044 cursor = QtGui.QTextCursor(block)
1044 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1045 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1045 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1046 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1046 QtGui.QTextCursor.KeepAnchor)
1047 QtGui.QTextCursor.KeepAnchor)
1047 return str(cursor.selection().toPlainText())
1048 return str(cursor.selection().toPlainText())
1048
1049
1049 def _get_cursor(self):
1050 def _get_cursor(self):
1050 """ Convenience method that returns a cursor for the current position.
1051 """ Convenience method that returns a cursor for the current position.
1051 """
1052 """
1052 return self._control.textCursor()
1053 return self._control.textCursor()
1053
1054
1054 def _get_end_cursor(self):
1055 def _get_end_cursor(self):
1055 """ Convenience method that returns a cursor for the last character.
1056 """ Convenience method that returns a cursor for the last character.
1056 """
1057 """
1057 cursor = self._control.textCursor()
1058 cursor = self._control.textCursor()
1058 cursor.movePosition(QtGui.QTextCursor.End)
1059 cursor.movePosition(QtGui.QTextCursor.End)
1059 return cursor
1060 return cursor
1060
1061
1061 def _get_input_buffer_cursor_column(self):
1062 def _get_input_buffer_cursor_column(self):
1062 """ Returns the column of the cursor in the input buffer, excluding the
1063 """ Returns the column of the cursor in the input buffer, excluding the
1063 contribution by the prompt, or -1 if there is no such column.
1064 contribution by the prompt, or -1 if there is no such column.
1064 """
1065 """
1065 prompt = self._get_input_buffer_cursor_prompt()
1066 prompt = self._get_input_buffer_cursor_prompt()
1066 if prompt is None:
1067 if prompt is None:
1067 return -1
1068 return -1
1068 else:
1069 else:
1069 cursor = self._control.textCursor()
1070 cursor = self._control.textCursor()
1070 return cursor.columnNumber() - len(prompt)
1071 return cursor.columnNumber() - len(prompt)
1071
1072
1072 def _get_input_buffer_cursor_line(self):
1073 def _get_input_buffer_cursor_line(self):
1073 """ Returns line of the input buffer that contains the cursor, or None
1074 """ Returns line of the input buffer that contains the cursor, or None
1074 if there is no such line.
1075 if there is no such line.
1075 """
1076 """
1076 prompt = self._get_input_buffer_cursor_prompt()
1077 prompt = self._get_input_buffer_cursor_prompt()
1077 if prompt is None:
1078 if prompt is None:
1078 return None
1079 return None
1079 else:
1080 else:
1080 cursor = self._control.textCursor()
1081 cursor = self._control.textCursor()
1081 text = self._get_block_plain_text(cursor.block())
1082 text = self._get_block_plain_text(cursor.block())
1082 return text[len(prompt):]
1083 return text[len(prompt):]
1083
1084
1084 def _get_input_buffer_cursor_prompt(self):
1085 def _get_input_buffer_cursor_prompt(self):
1085 """ Returns the (plain text) prompt for line of the input buffer that
1086 """ Returns the (plain text) prompt for line of the input buffer that
1086 contains the cursor, or None if there is no such line.
1087 contains the cursor, or None if there is no such line.
1087 """
1088 """
1088 if self._executing:
1089 if self._executing:
1089 return None
1090 return None
1090 cursor = self._control.textCursor()
1091 cursor = self._control.textCursor()
1091 if cursor.position() >= self._prompt_pos:
1092 if cursor.position() >= self._prompt_pos:
1092 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1093 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1093 return self._prompt
1094 return self._prompt
1094 else:
1095 else:
1095 return self._continuation_prompt
1096 return self._continuation_prompt
1096 else:
1097 else:
1097 return None
1098 return None
1098
1099
1099 def _get_prompt_cursor(self):
1100 def _get_prompt_cursor(self):
1100 """ Convenience method that returns a cursor for the prompt position.
1101 """ Convenience method that returns a cursor for the prompt position.
1101 """
1102 """
1102 cursor = self._control.textCursor()
1103 cursor = self._control.textCursor()
1103 cursor.setPosition(self._prompt_pos)
1104 cursor.setPosition(self._prompt_pos)
1104 return cursor
1105 return cursor
1105
1106
1106 def _get_selection_cursor(self, start, end):
1107 def _get_selection_cursor(self, start, end):
1107 """ Convenience method that returns a cursor with text selected between
1108 """ Convenience method that returns a cursor with text selected between
1108 the positions 'start' and 'end'.
1109 the positions 'start' and 'end'.
1109 """
1110 """
1110 cursor = self._control.textCursor()
1111 cursor = self._control.textCursor()
1111 cursor.setPosition(start)
1112 cursor.setPosition(start)
1112 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1113 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1113 return cursor
1114 return cursor
1114
1115
1115 def _get_word_start_cursor(self, position):
1116 def _get_word_start_cursor(self, position):
1116 """ Find the start of the word to the left the given position. If a
1117 """ Find the start of the word to the left the given position. If a
1117 sequence of non-word characters precedes the first word, skip over
1118 sequence of non-word characters precedes the first word, skip over
1118 them. (This emulates the behavior of bash, emacs, etc.)
1119 them. (This emulates the behavior of bash, emacs, etc.)
1119 """
1120 """
1120 document = self._control.document()
1121 document = self._control.document()
1121 position -= 1
1122 position -= 1
1122 while position >= self._prompt_pos and \
1123 while position >= self._prompt_pos and \
1123 not document.characterAt(position).isLetterOrNumber():
1124 not document.characterAt(position).isLetterOrNumber():
1124 position -= 1
1125 position -= 1
1125 while position >= self._prompt_pos and \
1126 while position >= self._prompt_pos and \
1126 document.characterAt(position).isLetterOrNumber():
1127 document.characterAt(position).isLetterOrNumber():
1127 position -= 1
1128 position -= 1
1128 cursor = self._control.textCursor()
1129 cursor = self._control.textCursor()
1129 cursor.setPosition(position + 1)
1130 cursor.setPosition(position + 1)
1130 return cursor
1131 return cursor
1131
1132
1132 def _get_word_end_cursor(self, position):
1133 def _get_word_end_cursor(self, position):
1133 """ Find the end of the word to the right the given position. If a
1134 """ Find the end of the word to the right the given position. If a
1134 sequence of non-word characters precedes the first word, skip over
1135 sequence of non-word characters precedes the first word, skip over
1135 them. (This emulates the behavior of bash, emacs, etc.)
1136 them. (This emulates the behavior of bash, emacs, etc.)
1136 """
1137 """
1137 document = self._control.document()
1138 document = self._control.document()
1138 end = self._get_end_cursor().position()
1139 end = self._get_end_cursor().position()
1139 while position < end and \
1140 while position < end and \
1140 not document.characterAt(position).isLetterOrNumber():
1141 not document.characterAt(position).isLetterOrNumber():
1141 position += 1
1142 position += 1
1142 while position < end and \
1143 while position < end and \
1143 document.characterAt(position).isLetterOrNumber():
1144 document.characterAt(position).isLetterOrNumber():
1144 position += 1
1145 position += 1
1145 cursor = self._control.textCursor()
1146 cursor = self._control.textCursor()
1146 cursor.setPosition(position)
1147 cursor.setPosition(position)
1147 return cursor
1148 return cursor
1148
1149
1149 def _insert_continuation_prompt(self, cursor):
1150 def _insert_continuation_prompt(self, cursor):
1150 """ Inserts new continuation prompt using the specified cursor.
1151 """ Inserts new continuation prompt using the specified cursor.
1151 """
1152 """
1152 if self._continuation_prompt_html is None:
1153 if self._continuation_prompt_html is None:
1153 self._insert_plain_text(cursor, self._continuation_prompt)
1154 self._insert_plain_text(cursor, self._continuation_prompt)
1154 else:
1155 else:
1155 self._continuation_prompt = self._insert_html_fetching_plain_text(
1156 self._continuation_prompt = self._insert_html_fetching_plain_text(
1156 cursor, self._continuation_prompt_html)
1157 cursor, self._continuation_prompt_html)
1157
1158
1158 def _insert_html(self, cursor, html):
1159 def _insert_html(self, cursor, html):
1159 """ Inserts HTML using the specified cursor in such a way that future
1160 """ Inserts HTML using the specified cursor in such a way that future
1160 formatting is unaffected.
1161 formatting is unaffected.
1161 """
1162 """
1162 cursor.beginEditBlock()
1163 cursor.beginEditBlock()
1163 cursor.insertHtml(html)
1164 cursor.insertHtml(html)
1164
1165
1165 # After inserting HTML, the text document "remembers" it's in "html
1166 # After inserting HTML, the text document "remembers" it's in "html
1166 # mode", which means that subsequent calls adding plain text will result
1167 # mode", which means that subsequent calls adding plain text will result
1167 # in unwanted formatting, lost tab characters, etc. The following code
1168 # in unwanted formatting, lost tab characters, etc. The following code
1168 # hacks around this behavior, which I consider to be a bug in Qt, by
1169 # hacks around this behavior, which I consider to be a bug in Qt, by
1169 # (crudely) resetting the document's style state.
1170 # (crudely) resetting the document's style state.
1170 cursor.movePosition(QtGui.QTextCursor.Left,
1171 cursor.movePosition(QtGui.QTextCursor.Left,
1171 QtGui.QTextCursor.KeepAnchor)
1172 QtGui.QTextCursor.KeepAnchor)
1172 if cursor.selection().toPlainText() == ' ':
1173 if cursor.selection().toPlainText() == ' ':
1173 cursor.removeSelectedText()
1174 cursor.removeSelectedText()
1174 else:
1175 else:
1175 cursor.movePosition(QtGui.QTextCursor.Right)
1176 cursor.movePosition(QtGui.QTextCursor.Right)
1176 cursor.insertText(' ', QtGui.QTextCharFormat())
1177 cursor.insertText(' ', QtGui.QTextCharFormat())
1177 cursor.endEditBlock()
1178 cursor.endEditBlock()
1178
1179
1179 def _insert_html_fetching_plain_text(self, cursor, html):
1180 def _insert_html_fetching_plain_text(self, cursor, html):
1180 """ Inserts HTML using the specified cursor, then returns its plain text
1181 """ Inserts HTML using the specified cursor, then returns its plain text
1181 version.
1182 version.
1182 """
1183 """
1183 cursor.beginEditBlock()
1184 cursor.beginEditBlock()
1184 cursor.removeSelectedText()
1185 cursor.removeSelectedText()
1185
1186
1186 start = cursor.position()
1187 start = cursor.position()
1187 self._insert_html(cursor, html)
1188 self._insert_html(cursor, html)
1188 end = cursor.position()
1189 end = cursor.position()
1189 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1190 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1190 text = str(cursor.selection().toPlainText())
1191 text = str(cursor.selection().toPlainText())
1191
1192
1192 cursor.setPosition(end)
1193 cursor.setPosition(end)
1193 cursor.endEditBlock()
1194 cursor.endEditBlock()
1194 return text
1195 return text
1195
1196
1196 def _insert_plain_text(self, cursor, text):
1197 def _insert_plain_text(self, cursor, text):
1197 """ Inserts plain text using the specified cursor, processing ANSI codes
1198 """ Inserts plain text using the specified cursor, processing ANSI codes
1198 if enabled.
1199 if enabled.
1199 """
1200 """
1200 cursor.beginEditBlock()
1201 cursor.beginEditBlock()
1201 if self.ansi_codes:
1202 if self.ansi_codes:
1202 for substring in self._ansi_processor.split_string(text):
1203 for substring in self._ansi_processor.split_string(text):
1203 for action in self._ansi_processor.actions:
1204 for action in self._ansi_processor.actions:
1204 if action.kind == 'erase' and action.area == 'screen':
1205 if action.kind == 'erase' and action.area == 'screen':
1205 cursor.select(QtGui.QTextCursor.Document)
1206 cursor.select(QtGui.QTextCursor.Document)
1206 cursor.removeSelectedText()
1207 cursor.removeSelectedText()
1207 format = self._ansi_processor.get_format()
1208 format = self._ansi_processor.get_format()
1208 cursor.insertText(substring, format)
1209 cursor.insertText(substring, format)
1209 else:
1210 else:
1210 cursor.insertText(text)
1211 cursor.insertText(text)
1211 cursor.endEditBlock()
1212 cursor.endEditBlock()
1212
1213
1213 def _insert_plain_text_into_buffer(self, text):
1214 def _insert_plain_text_into_buffer(self, text):
1214 """ Inserts text into the input buffer at the current cursor position,
1215 """ Inserts text into the input buffer at the current cursor position,
1215 ensuring that continuation prompts are inserted as necessary.
1216 ensuring that continuation prompts are inserted as necessary.
1216 """
1217 """
1217 lines = str(text).splitlines(True)
1218 lines = str(text).splitlines(True)
1218 if lines:
1219 if lines:
1219 self._keep_cursor_in_buffer()
1220 self._keep_cursor_in_buffer()
1220 cursor = self._control.textCursor()
1221 cursor = self._control.textCursor()
1221 cursor.beginEditBlock()
1222 cursor.beginEditBlock()
1222 cursor.insertText(lines[0])
1223 cursor.insertText(lines[0])
1223 for line in lines[1:]:
1224 for line in lines[1:]:
1224 if self._continuation_prompt_html is None:
1225 if self._continuation_prompt_html is None:
1225 cursor.insertText(self._continuation_prompt)
1226 cursor.insertText(self._continuation_prompt)
1226 else:
1227 else:
1227 self._continuation_prompt = \
1228 self._continuation_prompt = \
1228 self._insert_html_fetching_plain_text(
1229 self._insert_html_fetching_plain_text(
1229 cursor, self._continuation_prompt_html)
1230 cursor, self._continuation_prompt_html)
1230 cursor.insertText(line)
1231 cursor.insertText(line)
1231 cursor.endEditBlock()
1232 cursor.endEditBlock()
1232 self._control.setTextCursor(cursor)
1233 self._control.setTextCursor(cursor)
1233
1234
1234 def _in_buffer(self, position=None):
1235 def _in_buffer(self, position=None):
1235 """ Returns whether the current cursor (or, if specified, a position) is
1236 """ Returns whether the current cursor (or, if specified, a position) is
1236 inside the editing region.
1237 inside the editing region.
1237 """
1238 """
1238 cursor = self._control.textCursor()
1239 cursor = self._control.textCursor()
1239 if position is None:
1240 if position is None:
1240 position = cursor.position()
1241 position = cursor.position()
1241 else:
1242 else:
1242 cursor.setPosition(position)
1243 cursor.setPosition(position)
1243 line = cursor.blockNumber()
1244 line = cursor.blockNumber()
1244 prompt_line = self._get_prompt_cursor().blockNumber()
1245 prompt_line = self._get_prompt_cursor().blockNumber()
1245 if line == prompt_line:
1246 if line == prompt_line:
1246 return position >= self._prompt_pos
1247 return position >= self._prompt_pos
1247 elif line > prompt_line:
1248 elif line > prompt_line:
1248 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1249 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1249 prompt_pos = cursor.position() + len(self._continuation_prompt)
1250 prompt_pos = cursor.position() + len(self._continuation_prompt)
1250 return position >= prompt_pos
1251 return position >= prompt_pos
1251 return False
1252 return False
1252
1253
1253 def _keep_cursor_in_buffer(self):
1254 def _keep_cursor_in_buffer(self):
1254 """ Ensures that the cursor is inside the editing region. Returns
1255 """ Ensures that the cursor is inside the editing region. Returns
1255 whether the cursor was moved.
1256 whether the cursor was moved.
1256 """
1257 """
1257 moved = not self._in_buffer()
1258 moved = not self._in_buffer()
1258 if moved:
1259 if moved:
1259 cursor = self._control.textCursor()
1260 cursor = self._control.textCursor()
1260 cursor.movePosition(QtGui.QTextCursor.End)
1261 cursor.movePosition(QtGui.QTextCursor.End)
1261 self._control.setTextCursor(cursor)
1262 self._control.setTextCursor(cursor)
1262 return moved
1263 return moved
1263
1264
1264 def _keyboard_quit(self):
1265 def _keyboard_quit(self):
1265 """ Cancels the current editing task ala Ctrl-G in Emacs.
1266 """ Cancels the current editing task ala Ctrl-G in Emacs.
1266 """
1267 """
1267 if self._text_completing_pos:
1268 if self._text_completing_pos:
1268 self._cancel_text_completion()
1269 self._cancel_text_completion()
1269 else:
1270 else:
1270 self.input_buffer = ''
1271 self.input_buffer = ''
1271
1272
1272 def _page(self, text):
1273 def _page(self, text):
1273 """ Displays text using the pager if it exceeds the height of the
1274 """ Displays text using the pager if it exceeds the height of the
1274 visible area.
1275 visible area.
1275 """
1276 """
1276 if self.paging == 'none':
1277 if self.paging == 'none':
1277 self._append_plain_text(text)
1278 self._append_plain_text(text)
1278 else:
1279 else:
1279 line_height = QtGui.QFontMetrics(self.font).height()
1280 line_height = QtGui.QFontMetrics(self.font).height()
1280 minlines = self._control.viewport().height() / line_height
1281 minlines = self._control.viewport().height() / line_height
1281 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1282 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1282 if self.paging == 'custom':
1283 if self.paging == 'custom':
1283 self.custom_page_requested.emit(text)
1284 self.custom_page_requested.emit(text)
1284 else:
1285 else:
1285 self._page_control.clear()
1286 self._page_control.clear()
1286 cursor = self._page_control.textCursor()
1287 cursor = self._page_control.textCursor()
1287 self._insert_plain_text(cursor, text)
1288 self._insert_plain_text(cursor, text)
1288 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1289 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1289
1290
1290 self._page_control.viewport().resize(self._control.size())
1291 self._page_control.viewport().resize(self._control.size())
1291 if self._splitter:
1292 if self._splitter:
1292 self._page_control.show()
1293 self._page_control.show()
1293 self._page_control.setFocus()
1294 self._page_control.setFocus()
1294 else:
1295 else:
1295 self.layout().setCurrentWidget(self._page_control)
1296 self.layout().setCurrentWidget(self._page_control)
1296 else:
1297 else:
1297 self._append_plain_text(text)
1298 self._append_plain_text(text)
1298
1299
1299 def _prompt_started(self):
1300 def _prompt_started(self):
1300 """ Called immediately after a new prompt is displayed.
1301 """ Called immediately after a new prompt is displayed.
1301 """
1302 """
1302 # Temporarily disable the maximum block count to permit undo/redo and
1303 # Temporarily disable the maximum block count to permit undo/redo and
1303 # to ensure that the prompt position does not change due to truncation.
1304 # to ensure that the prompt position does not change due to truncation.
1304 # Because setting this property clears the undo/redo history, we only
1305 # Because setting this property clears the undo/redo history, we only
1305 # set it if we have to.
1306 # set it if we have to.
1306 if self._control.document().maximumBlockCount() > 0:
1307 if self._control.document().maximumBlockCount() > 0:
1307 self._control.document().setMaximumBlockCount(0)
1308 self._control.document().setMaximumBlockCount(0)
1308 self._control.setUndoRedoEnabled(True)
1309 self._control.setUndoRedoEnabled(True)
1309
1310
1310 self._control.setReadOnly(False)
1311 self._control.setReadOnly(False)
1311 self._control.moveCursor(QtGui.QTextCursor.End)
1312 self._control.moveCursor(QtGui.QTextCursor.End)
1312
1313
1313 self._executing = False
1314 self._executing = False
1314 self._prompt_started_hook()
1315 self._prompt_started_hook()
1315
1316
1316 def _prompt_finished(self):
1317 def _prompt_finished(self):
1317 """ Called immediately after a prompt is finished, i.e. when some input
1318 """ Called immediately after a prompt is finished, i.e. when some input
1318 will be processed and a new prompt displayed.
1319 will be processed and a new prompt displayed.
1319 """
1320 """
1320 self._control.setReadOnly(True)
1321 self._control.setReadOnly(True)
1321 self._prompt_finished_hook()
1322 self._prompt_finished_hook()
1322
1323
1323 def _readline(self, prompt='', callback=None):
1324 def _readline(self, prompt='', callback=None):
1324 """ Reads one line of input from the user.
1325 """ Reads one line of input from the user.
1325
1326
1326 Parameters
1327 Parameters
1327 ----------
1328 ----------
1328 prompt : str, optional
1329 prompt : str, optional
1329 The prompt to print before reading the line.
1330 The prompt to print before reading the line.
1330
1331
1331 callback : callable, optional
1332 callback : callable, optional
1332 A callback to execute with the read line. If not specified, input is
1333 A callback to execute with the read line. If not specified, input is
1333 read *synchronously* and this method does not return until it has
1334 read *synchronously* and this method does not return until it has
1334 been read.
1335 been read.
1335
1336
1336 Returns
1337 Returns
1337 -------
1338 -------
1338 If a callback is specified, returns nothing. Otherwise, returns the
1339 If a callback is specified, returns nothing. Otherwise, returns the
1339 input string with the trailing newline stripped.
1340 input string with the trailing newline stripped.
1340 """
1341 """
1341 if self._reading:
1342 if self._reading:
1342 raise RuntimeError('Cannot read a line. Widget is already reading.')
1343 raise RuntimeError('Cannot read a line. Widget is already reading.')
1343
1344
1344 if not callback and not self.isVisible():
1345 if not callback and not self.isVisible():
1345 # If the user cannot see the widget, this function cannot return.
1346 # If the user cannot see the widget, this function cannot return.
1346 raise RuntimeError('Cannot synchronously read a line if the widget '
1347 raise RuntimeError('Cannot synchronously read a line if the widget '
1347 'is not visible!')
1348 'is not visible!')
1348
1349
1349 self._reading = True
1350 self._reading = True
1350 self._show_prompt(prompt, newline=False)
1351 self._show_prompt(prompt, newline=False)
1351
1352
1352 if callback is None:
1353 if callback is None:
1353 self._reading_callback = None
1354 self._reading_callback = None
1354 while self._reading:
1355 while self._reading:
1355 QtCore.QCoreApplication.processEvents()
1356 QtCore.QCoreApplication.processEvents()
1356 return self.input_buffer.rstrip('\n')
1357 return self.input_buffer.rstrip('\n')
1357
1358
1358 else:
1359 else:
1359 self._reading_callback = lambda: \
1360 self._reading_callback = lambda: \
1360 callback(self.input_buffer.rstrip('\n'))
1361 callback(self.input_buffer.rstrip('\n'))
1361
1362
1362 def _set_continuation_prompt(self, prompt, html=False):
1363 def _set_continuation_prompt(self, prompt, html=False):
1363 """ Sets the continuation prompt.
1364 """ Sets the continuation prompt.
1364
1365
1365 Parameters
1366 Parameters
1366 ----------
1367 ----------
1367 prompt : str
1368 prompt : str
1368 The prompt to show when more input is needed.
1369 The prompt to show when more input is needed.
1369
1370
1370 html : bool, optional (default False)
1371 html : bool, optional (default False)
1371 If set, the prompt will be inserted as formatted HTML. Otherwise,
1372 If set, the prompt will be inserted as formatted HTML. Otherwise,
1372 the prompt will be treated as plain text, though ANSI color codes
1373 the prompt will be treated as plain text, though ANSI color codes
1373 will be handled.
1374 will be handled.
1374 """
1375 """
1375 if html:
1376 if html:
1376 self._continuation_prompt_html = prompt
1377 self._continuation_prompt_html = prompt
1377 else:
1378 else:
1378 self._continuation_prompt = prompt
1379 self._continuation_prompt = prompt
1379 self._continuation_prompt_html = None
1380 self._continuation_prompt_html = None
1380
1381
1381 def _set_cursor(self, cursor):
1382 def _set_cursor(self, cursor):
1382 """ Convenience method to set the current cursor.
1383 """ Convenience method to set the current cursor.
1383 """
1384 """
1384 self._control.setTextCursor(cursor)
1385 self._control.setTextCursor(cursor)
1385
1386
1386 def _show_context_menu(self, pos):
1387 def _show_context_menu(self, pos):
1387 """ Shows a context menu at the given QPoint (in widget coordinates).
1388 """ Shows a context menu at the given QPoint (in widget coordinates).
1388 """
1389 """
1389 menu = QtGui.QMenu()
1390 menu = QtGui.QMenu()
1390
1391
1391 copy_action = menu.addAction('Copy', self.copy)
1392 copy_action = menu.addAction('Copy', self.copy)
1392 copy_action.setEnabled(self._get_cursor().hasSelection())
1393 copy_action.setEnabled(self._get_cursor().hasSelection())
1393 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1394 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1394
1395
1395 paste_action = menu.addAction('Paste', self.paste)
1396 paste_action = menu.addAction('Paste', self.paste)
1396 paste_action.setEnabled(self.can_paste())
1397 paste_action.setEnabled(self.can_paste())
1397 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1398 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1398
1399
1399 menu.addSeparator()
1400 menu.addSeparator()
1400 menu.addAction('Select All', self.select_all)
1401 menu.addAction('Select All', self.select_all)
1401
1402
1402 menu.exec_(self._control.mapToGlobal(pos))
1403 menu.exec_(self._control.mapToGlobal(pos))
1403
1404
1404 def _show_prompt(self, prompt=None, html=False, newline=True):
1405 def _show_prompt(self, prompt=None, html=False, newline=True):
1405 """ Writes a new prompt at the end of the buffer.
1406 """ Writes a new prompt at the end of the buffer.
1406
1407
1407 Parameters
1408 Parameters
1408 ----------
1409 ----------
1409 prompt : str, optional
1410 prompt : str, optional
1410 The prompt to show. If not specified, the previous prompt is used.
1411 The prompt to show. If not specified, the previous prompt is used.
1411
1412
1412 html : bool, optional (default False)
1413 html : bool, optional (default False)
1413 Only relevant when a prompt is specified. If set, the prompt will
1414 Only relevant when a prompt is specified. If set, the prompt will
1414 be inserted as formatted HTML. Otherwise, the prompt will be treated
1415 be inserted as formatted HTML. Otherwise, the prompt will be treated
1415 as plain text, though ANSI color codes will be handled.
1416 as plain text, though ANSI color codes will be handled.
1416
1417
1417 newline : bool, optional (default True)
1418 newline : bool, optional (default True)
1418 If set, a new line will be written before showing the prompt if
1419 If set, a new line will be written before showing the prompt if
1419 there is not already a newline at the end of the buffer.
1420 there is not already a newline at the end of the buffer.
1420 """
1421 """
1421 # Insert a preliminary newline, if necessary.
1422 # Insert a preliminary newline, if necessary.
1422 if newline:
1423 if newline:
1423 cursor = self._get_end_cursor()
1424 cursor = self._get_end_cursor()
1424 if cursor.position() > 0:
1425 if cursor.position() > 0:
1425 cursor.movePosition(QtGui.QTextCursor.Left,
1426 cursor.movePosition(QtGui.QTextCursor.Left,
1426 QtGui.QTextCursor.KeepAnchor)
1427 QtGui.QTextCursor.KeepAnchor)
1427 if str(cursor.selection().toPlainText()) != '\n':
1428 if str(cursor.selection().toPlainText()) != '\n':
1428 self._append_plain_text('\n')
1429 self._append_plain_text('\n')
1429
1430
1430 # Write the prompt.
1431 # Write the prompt.
1431 self._append_plain_text(self._prompt_sep)
1432 self._append_plain_text(self._prompt_sep)
1432 if prompt is None:
1433 if prompt is None:
1433 if self._prompt_html is None:
1434 if self._prompt_html is None:
1434 self._append_plain_text(self._prompt)
1435 self._append_plain_text(self._prompt)
1435 else:
1436 else:
1436 self._append_html(self._prompt_html)
1437 self._append_html(self._prompt_html)
1437 else:
1438 else:
1438 if html:
1439 if html:
1439 self._prompt = self._append_html_fetching_plain_text(prompt)
1440 self._prompt = self._append_html_fetching_plain_text(prompt)
1440 self._prompt_html = prompt
1441 self._prompt_html = prompt
1441 else:
1442 else:
1442 self._append_plain_text(prompt)
1443 self._append_plain_text(prompt)
1443 self._prompt = prompt
1444 self._prompt = prompt
1444 self._prompt_html = None
1445 self._prompt_html = None
1445
1446
1446 self._prompt_pos = self._get_end_cursor().position()
1447 self._prompt_pos = self._get_end_cursor().position()
1447 self._prompt_started()
1448 self._prompt_started()
1448
1449
1449 #------ Signal handlers ----------------------------------------------------
1450 #------ Signal handlers ----------------------------------------------------
1450
1451
1451 def _cursor_position_changed(self):
1452 def _cursor_position_changed(self):
1452 """ Clears the temporary buffer based on the cursor position.
1453 """ Clears the temporary buffer based on the cursor position.
1453 """
1454 """
1454 if self._text_completing_pos:
1455 if self._text_completing_pos:
1455 document = self._control.document()
1456 document = self._control.document()
1456 if self._text_completing_pos < document.characterCount():
1457 if self._text_completing_pos < document.characterCount():
1457 cursor = self._control.textCursor()
1458 cursor = self._control.textCursor()
1458 pos = cursor.position()
1459 pos = cursor.position()
1459 text_cursor = self._control.textCursor()
1460 text_cursor = self._control.textCursor()
1460 text_cursor.setPosition(self._text_completing_pos)
1461 text_cursor.setPosition(self._text_completing_pos)
1461 if pos < self._text_completing_pos or \
1462 if pos < self._text_completing_pos or \
1462 cursor.blockNumber() > text_cursor.blockNumber():
1463 cursor.blockNumber() > text_cursor.blockNumber():
1463 self._clear_temporary_buffer()
1464 self._clear_temporary_buffer()
1464 self._text_completing_pos = 0
1465 self._text_completing_pos = 0
1465 else:
1466 else:
1466 self._clear_temporary_buffer()
1467 self._clear_temporary_buffer()
1467 self._text_completing_pos = 0
1468 self._text_completing_pos = 0
1468
1469
1469
1470
1470 class HistoryConsoleWidget(ConsoleWidget):
1471 class HistoryConsoleWidget(ConsoleWidget):
1471 """ A ConsoleWidget that keeps a history of the commands that have been
1472 """ A ConsoleWidget that keeps a history of the commands that have been
1472 executed.
1473 executed.
1473 """
1474 """
1474
1475
1475 #---------------------------------------------------------------------------
1476 #---------------------------------------------------------------------------
1476 # 'object' interface
1477 # 'object' interface
1477 #---------------------------------------------------------------------------
1478 #---------------------------------------------------------------------------
1478
1479
1479 def __init__(self, *args, **kw):
1480 def __init__(self, *args, **kw):
1480 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1481 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1481 self._history = []
1482 self._history = []
1482 self._history_index = 0
1483 self._history_index = 0
1483
1484
1484 #---------------------------------------------------------------------------
1485 #---------------------------------------------------------------------------
1485 # 'ConsoleWidget' public interface
1486 # 'ConsoleWidget' public interface
1486 #---------------------------------------------------------------------------
1487 #---------------------------------------------------------------------------
1487
1488
1488 def execute(self, source=None, hidden=False, interactive=False):
1489 def execute(self, source=None, hidden=False, interactive=False):
1489 """ Reimplemented to the store history.
1490 """ Reimplemented to the store history.
1490 """
1491 """
1491 if not hidden:
1492 if not hidden:
1492 history = self.input_buffer if source is None else source
1493 history = self.input_buffer if source is None else source
1493
1494
1494 executed = super(HistoryConsoleWidget, self).execute(
1495 executed = super(HistoryConsoleWidget, self).execute(
1495 source, hidden, interactive)
1496 source, hidden, interactive)
1496
1497
1497 if executed and not hidden:
1498 if executed and not hidden:
1498 # Save the command unless it was a blank line.
1499 # Save the command unless it was a blank line.
1499 history = history.rstrip()
1500 history = history.rstrip()
1500 if history:
1501 if history:
1501 self._history.append(history)
1502 self._history.append(history)
1502 self._history_index = len(self._history)
1503 self._history_index = len(self._history)
1503
1504
1504 return executed
1505 return executed
1505
1506
1506 #---------------------------------------------------------------------------
1507 #---------------------------------------------------------------------------
1507 # 'ConsoleWidget' abstract interface
1508 # 'ConsoleWidget' abstract interface
1508 #---------------------------------------------------------------------------
1509 #---------------------------------------------------------------------------
1509
1510
1510 def _up_pressed(self):
1511 def _up_pressed(self):
1511 """ Called when the up key is pressed. Returns whether to continue
1512 """ Called when the up key is pressed. Returns whether to continue
1512 processing the event.
1513 processing the event.
1513 """
1514 """
1514 prompt_cursor = self._get_prompt_cursor()
1515 prompt_cursor = self._get_prompt_cursor()
1515 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1516 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1516 self.history_previous()
1517 self.history_previous()
1517
1518
1518 # Go to the first line of prompt for seemless history scrolling.
1519 # Go to the first line of prompt for seemless history scrolling.
1519 cursor = self._get_prompt_cursor()
1520 cursor = self._get_prompt_cursor()
1520 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1521 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1521 self._set_cursor(cursor)
1522 self._set_cursor(cursor)
1522
1523
1523 return False
1524 return False
1524 return True
1525 return True
1525
1526
1526 def _down_pressed(self):
1527 def _down_pressed(self):
1527 """ Called when the down key is pressed. Returns whether to continue
1528 """ Called when the down key is pressed. Returns whether to continue
1528 processing the event.
1529 processing the event.
1529 """
1530 """
1530 end_cursor = self._get_end_cursor()
1531 end_cursor = self._get_end_cursor()
1531 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1532 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1532 self.history_next()
1533 self.history_next()
1533 return False
1534 return False
1534 return True
1535 return True
1535
1536
1536 #---------------------------------------------------------------------------
1537 #---------------------------------------------------------------------------
1537 # 'HistoryConsoleWidget' public interface
1538 # 'HistoryConsoleWidget' public interface
1538 #---------------------------------------------------------------------------
1539 #---------------------------------------------------------------------------
1539
1540
1540 def history_previous(self):
1541 def history_previous(self):
1541 """ If possible, set the input buffer to the previous item in the
1542 """ If possible, set the input buffer to the previous item in the
1542 history.
1543 history.
1543 """
1544 """
1544 if self._history_index > 0:
1545 if self._history_index > 0:
1545 self._history_index -= 1
1546 self._history_index -= 1
1546 self.input_buffer = self._history[self._history_index]
1547 self.input_buffer = self._history[self._history_index]
1547
1548
1548 def history_next(self):
1549 def history_next(self):
1549 """ Set the input buffer to the next item in the history, or a blank
1550 """ Set the input buffer to the next item in the history, or a blank
1550 line if there is no subsequent item.
1551 line if there is no subsequent item.
1551 """
1552 """
1552 if self._history_index < len(self._history):
1553 if self._history_index < len(self._history):
1553 self._history_index += 1
1554 self._history_index += 1
1554 if self._history_index < len(self._history):
1555 if self._history_index < len(self._history):
1555 self.input_buffer = self._history[self._history_index]
1556 self.input_buffer = self._history[self._history_index]
1556 else:
1557 else:
1557 self.input_buffer = ''
1558 self.input_buffer = ''
1558
1559
1559 #---------------------------------------------------------------------------
1560 #---------------------------------------------------------------------------
1560 # 'HistoryConsoleWidget' protected interface
1561 # 'HistoryConsoleWidget' protected interface
1561 #---------------------------------------------------------------------------
1562 #---------------------------------------------------------------------------
1562
1563
1563 def _set_history(self, history):
1564 def _set_history(self, history):
1564 """ Replace the current history with a sequence of history items.
1565 """ Replace the current history with a sequence of history items.
1565 """
1566 """
1566 self._history = list(history)
1567 self._history = list(history)
1567 self._history_index = len(self._history)
1568 self._history_index = len(self._history)
@@ -1,512 +1,467 b''
1 # Standard library imports
1 # Standard library imports
2 from collections import namedtuple
2 from collections import namedtuple
3 import signal
3 import signal
4 import sys
4 import sys
5 import time
6
5
7 # System library imports
6 # System library imports
8 from pygments.lexers import PythonLexer
7 from pygments.lexers import PythonLexer
9 from PyQt4 import QtCore, QtGui
8 from PyQt4 import QtCore, QtGui
10
9
11 # Local imports
10 # Local imports
12 from IPython.core.inputsplitter import InputSplitter
11 from IPython.core.inputsplitter import InputSplitter
13 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
14 from IPython.utils.traitlets import Bool
13 from IPython.utils.traitlets import Bool
15 from bracket_matcher import BracketMatcher
14 from bracket_matcher import BracketMatcher
16 from call_tip_widget import CallTipWidget
15 from call_tip_widget import CallTipWidget
17 from completion_lexer import CompletionLexer
16 from completion_lexer import CompletionLexer
18 from console_widget import HistoryConsoleWidget
17 from console_widget import HistoryConsoleWidget
19 from pygments_highlighter import PygmentsHighlighter
18 from pygments_highlighter import PygmentsHighlighter
20
19
21
20
22 class FrontendHighlighter(PygmentsHighlighter):
21 class FrontendHighlighter(PygmentsHighlighter):
23 """ A PygmentsHighlighter that can be turned on and off and that ignores
22 """ A PygmentsHighlighter that can be turned on and off and that ignores
24 prompts.
23 prompts.
25 """
24 """
26
25
27 def __init__(self, frontend):
26 def __init__(self, frontend):
28 super(FrontendHighlighter, self).__init__(frontend._control.document())
27 super(FrontendHighlighter, self).__init__(frontend._control.document())
29 self._current_offset = 0
28 self._current_offset = 0
30 self._frontend = frontend
29 self._frontend = frontend
31 self.highlighting_on = False
30 self.highlighting_on = False
32
31
33 def highlightBlock(self, qstring):
32 def highlightBlock(self, qstring):
34 """ Highlight a block of text. Reimplemented to highlight selectively.
33 """ Highlight a block of text. Reimplemented to highlight selectively.
35 """
34 """
36 if not self.highlighting_on:
35 if not self.highlighting_on:
37 return
36 return
38
37
39 # The input to this function is unicode string that may contain
38 # The input to this function is unicode string that may contain
40 # paragraph break characters, non-breaking spaces, etc. Here we acquire
39 # paragraph break characters, non-breaking spaces, etc. Here we acquire
41 # the string as plain text so we can compare it.
40 # the string as plain text so we can compare it.
42 current_block = self.currentBlock()
41 current_block = self.currentBlock()
43 string = self._frontend._get_block_plain_text(current_block)
42 string = self._frontend._get_block_plain_text(current_block)
44
43
45 # Decide whether to check for the regular or continuation prompt.
44 # Decide whether to check for the regular or continuation prompt.
46 if current_block.contains(self._frontend._prompt_pos):
45 if current_block.contains(self._frontend._prompt_pos):
47 prompt = self._frontend._prompt
46 prompt = self._frontend._prompt
48 else:
47 else:
49 prompt = self._frontend._continuation_prompt
48 prompt = self._frontend._continuation_prompt
50
49
51 # Don't highlight the part of the string that contains the prompt.
50 # Don't highlight the part of the string that contains the prompt.
52 if string.startswith(prompt):
51 if string.startswith(prompt):
53 self._current_offset = len(prompt)
52 self._current_offset = len(prompt)
54 qstring.remove(0, len(prompt))
53 qstring.remove(0, len(prompt))
55 else:
54 else:
56 self._current_offset = 0
55 self._current_offset = 0
57
56
58 PygmentsHighlighter.highlightBlock(self, qstring)
57 PygmentsHighlighter.highlightBlock(self, qstring)
59
58
60 def rehighlightBlock(self, block):
59 def rehighlightBlock(self, block):
61 """ Reimplemented to temporarily enable highlighting if disabled.
60 """ Reimplemented to temporarily enable highlighting if disabled.
62 """
61 """
63 old = self.highlighting_on
62 old = self.highlighting_on
64 self.highlighting_on = True
63 self.highlighting_on = True
65 super(FrontendHighlighter, self).rehighlightBlock(block)
64 super(FrontendHighlighter, self).rehighlightBlock(block)
66 self.highlighting_on = old
65 self.highlighting_on = old
67
66
68 def setFormat(self, start, count, format):
67 def setFormat(self, start, count, format):
69 """ Reimplemented to highlight selectively.
68 """ Reimplemented to highlight selectively.
70 """
69 """
71 start += self._current_offset
70 start += self._current_offset
72 PygmentsHighlighter.setFormat(self, start, count, format)
71 PygmentsHighlighter.setFormat(self, start, count, format)
73
72
74
73
75 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
74 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
76 """ A Qt frontend for a generic Python kernel.
75 """ A Qt frontend for a generic Python kernel.
77 """
76 """
78
77
79 # An option and corresponding signal for overriding the default kernel
78 # An option and corresponding signal for overriding the default kernel
80 # interrupt behavior.
79 # interrupt behavior.
81 custom_interrupt = Bool(False)
80 custom_interrupt = Bool(False)
82 custom_interrupt_requested = QtCore.pyqtSignal()
81 custom_interrupt_requested = QtCore.pyqtSignal()
83
82
84 # An option and corresponding signals for overriding the default kernel
83 # An option and corresponding signals for overriding the default kernel
85 # restart behavior.
84 # restart behavior.
86 custom_restart = Bool(False)
85 custom_restart = Bool(False)
87 custom_restart_kernel_died = QtCore.pyqtSignal(float)
86 custom_restart_kernel_died = QtCore.pyqtSignal(float)
88 custom_restart_requested = QtCore.pyqtSignal()
87 custom_restart_requested = QtCore.pyqtSignal()
89
88
90 # Emitted when an 'execute_reply' has been received from the kernel and
89 # Emitted when an 'execute_reply' has been received from the kernel and
91 # processed by the FrontendWidget.
90 # processed by the FrontendWidget.
92 executed = QtCore.pyqtSignal(object)
91 executed = QtCore.pyqtSignal(object)
92
93 # Emitted when an exit request has been received from the kernel.
94 exit_requested = QtCore.pyqtSignal()
93
95
94 # Protected class variables.
96 # Protected class variables.
95 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
97 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
96 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
98 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
97 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
99 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
98 _input_splitter_class = InputSplitter
100 _input_splitter_class = InputSplitter
99
101
100 #---------------------------------------------------------------------------
102 #---------------------------------------------------------------------------
101 # 'object' interface
103 # 'object' interface
102 #---------------------------------------------------------------------------
104 #---------------------------------------------------------------------------
103
105
104 def __init__(self, *args, **kw):
106 def __init__(self, *args, **kw):
105 super(FrontendWidget, self).__init__(*args, **kw)
107 super(FrontendWidget, self).__init__(*args, **kw)
106
108
107 # FrontendWidget protected variables.
109 # FrontendWidget protected variables.
108 self._bracket_matcher = BracketMatcher(self._control)
110 self._bracket_matcher = BracketMatcher(self._control)
109 self._call_tip_widget = CallTipWidget(self._control)
111 self._call_tip_widget = CallTipWidget(self._control)
110 self._completion_lexer = CompletionLexer(PythonLexer())
112 self._completion_lexer = CompletionLexer(PythonLexer())
111 self._hidden = False
113 self._hidden = False
112 self._highlighter = FrontendHighlighter(self)
114 self._highlighter = FrontendHighlighter(self)
113 self._input_splitter = self._input_splitter_class(input_mode='block')
115 self._input_splitter = self._input_splitter_class(input_mode='block')
114 self._kernel_manager = None
116 self._kernel_manager = None
115 self._possible_kernel_restart = False
117 self._possible_kernel_restart = False
116 self._request_info = {}
118 self._request_info = {}
117
119
118 # Configure the ConsoleWidget.
120 # Configure the ConsoleWidget.
119 self.tab_width = 4
121 self.tab_width = 4
120 self._set_continuation_prompt('... ')
122 self._set_continuation_prompt('... ')
121
123
122 # Connect signal handlers.
124 # Connect signal handlers.
123 document = self._control.document()
125 document = self._control.document()
124 document.contentsChange.connect(self._document_contents_change)
126 document.contentsChange.connect(self._document_contents_change)
125
127
126 #---------------------------------------------------------------------------
128 #---------------------------------------------------------------------------
127 # 'ConsoleWidget' abstract interface
129 # 'ConsoleWidget' abstract interface
128 #---------------------------------------------------------------------------
130 #---------------------------------------------------------------------------
129
131
130 def _is_complete(self, source, interactive):
132 def _is_complete(self, source, interactive):
131 """ Returns whether 'source' can be completely processed and a new
133 """ Returns whether 'source' can be completely processed and a new
132 prompt created. When triggered by an Enter/Return key press,
134 prompt created. When triggered by an Enter/Return key press,
133 'interactive' is True; otherwise, it is False.
135 'interactive' is True; otherwise, it is False.
134 """
136 """
135 complete = self._input_splitter.push(source.expandtabs(4))
137 complete = self._input_splitter.push(source.expandtabs(4))
136 if interactive:
138 if interactive:
137 complete = not self._input_splitter.push_accepts_more()
139 complete = not self._input_splitter.push_accepts_more()
138 return complete
140 return complete
139
141
140 def _execute(self, source, hidden, user_variables=None,
142 def _execute(self, source, hidden):
141 user_expressions=None):
142 """ Execute 'source'. If 'hidden', do not show any output.
143 """ Execute 'source'. If 'hidden', do not show any output.
143
144
144 See parent class :meth:`execute` docstring for full details.
145 See parent class :meth:`execute` docstring for full details.
145 """
146 """
146 # tmp code for testing, disable in real use with 'if 0'. Only delete
147 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
147 # this code once we have automated tests for these fields.
148 if 0:
149 user_variables = ['x', 'y', 'z']
150 user_expressions = {'sum' : '1+1',
151 'bad syntax' : 'klsdafj kasd f',
152 'bad call' : 'range("hi")',
153 'time' : 'time.time()',
154 }
155 # /end tmp code
156
157 # FIXME - user_variables/expressions are not visible in API above us.
158 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden,
159 user_variables,
160 user_expressions)
161 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
148 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
162 self._hidden = hidden
149 self._hidden = hidden
163
150
164 def _prompt_started_hook(self):
151 def _prompt_started_hook(self):
165 """ Called immediately after a new prompt is displayed.
152 """ Called immediately after a new prompt is displayed.
166 """
153 """
167 if not self._reading:
154 if not self._reading:
168 self._highlighter.highlighting_on = True
155 self._highlighter.highlighting_on = True
169
156
170 def _prompt_finished_hook(self):
157 def _prompt_finished_hook(self):
171 """ Called immediately after a prompt is finished, i.e. when some input
158 """ Called immediately after a prompt is finished, i.e. when some input
172 will be processed and a new prompt displayed.
159 will be processed and a new prompt displayed.
173 """
160 """
174 if not self._reading:
161 if not self._reading:
175 self._highlighter.highlighting_on = False
162 self._highlighter.highlighting_on = False
176
163
177 def _tab_pressed(self):
164 def _tab_pressed(self):
178 """ Called when the tab key is pressed. Returns whether to continue
165 """ Called when the tab key is pressed. Returns whether to continue
179 processing the event.
166 processing the event.
180 """
167 """
181 # Perform tab completion if:
168 # Perform tab completion if:
182 # 1) The cursor is in the input buffer.
169 # 1) The cursor is in the input buffer.
183 # 2) There is a non-whitespace character before the cursor.
170 # 2) There is a non-whitespace character before the cursor.
184 text = self._get_input_buffer_cursor_line()
171 text = self._get_input_buffer_cursor_line()
185 if text is None:
172 if text is None:
186 return False
173 return False
187 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
174 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
188 if complete:
175 if complete:
189 self._complete()
176 self._complete()
190 return not complete
177 return not complete
191
178
192 #---------------------------------------------------------------------------
179 #---------------------------------------------------------------------------
193 # 'ConsoleWidget' protected interface
180 # 'ConsoleWidget' protected interface
194 #---------------------------------------------------------------------------
181 #---------------------------------------------------------------------------
195
182
196 def _event_filter_console_keypress(self, event):
183 def _event_filter_console_keypress(self, event):
197 """ Reimplemented to allow execution interruption.
184 """ Reimplemented to allow execution interruption.
198 """
185 """
199 key = event.key()
186 key = event.key()
200 if self._control_key_down(event.modifiers(), include_command=False):
187 if self._control_key_down(event.modifiers(), include_command=False):
201 if key == QtCore.Qt.Key_C and self._executing:
188 if key == QtCore.Qt.Key_C and self._executing:
202 self.interrupt_kernel()
189 self.interrupt_kernel()
203 return True
190 return True
204 elif key == QtCore.Qt.Key_Period:
191 elif key == QtCore.Qt.Key_Period:
205 message = 'Are you sure you want to restart the kernel?'
192 message = 'Are you sure you want to restart the kernel?'
206 self.restart_kernel(message)
193 self.restart_kernel(message)
207 return True
194 return True
208 return super(FrontendWidget, self)._event_filter_console_keypress(event)
195 return super(FrontendWidget, self)._event_filter_console_keypress(event)
209
196
210 def _insert_continuation_prompt(self, cursor):
197 def _insert_continuation_prompt(self, cursor):
211 """ Reimplemented for auto-indentation.
198 """ Reimplemented for auto-indentation.
212 """
199 """
213 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
200 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
214 spaces = self._input_splitter.indent_spaces
201 spaces = self._input_splitter.indent_spaces
215 cursor.insertText('\t' * (spaces / self.tab_width))
202 cursor.insertText('\t' * (spaces / self.tab_width))
216 cursor.insertText(' ' * (spaces % self.tab_width))
203 cursor.insertText(' ' * (spaces % self.tab_width))
217
204
218 #---------------------------------------------------------------------------
205 #---------------------------------------------------------------------------
219 # 'BaseFrontendMixin' abstract interface
206 # 'BaseFrontendMixin' abstract interface
220 #---------------------------------------------------------------------------
207 #---------------------------------------------------------------------------
221
208
222 def _handle_complete_reply(self, rep):
209 def _handle_complete_reply(self, rep):
223 """ Handle replies for tab completion.
210 """ Handle replies for tab completion.
224 """
211 """
225 cursor = self._get_cursor()
212 cursor = self._get_cursor()
226 info = self._request_info.get('complete')
213 info = self._request_info.get('complete')
227 if info and info.id == rep['parent_header']['msg_id'] and \
214 if info and info.id == rep['parent_header']['msg_id'] and \
228 info.pos == cursor.position():
215 info.pos == cursor.position():
229 text = '.'.join(self._get_context())
216 text = '.'.join(self._get_context())
230 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
217 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
231 self._complete_with_items(cursor, rep['content']['matches'])
218 self._complete_with_items(cursor, rep['content']['matches'])
232
219
233 def _handle_execute_reply(self, msg):
220 def _handle_execute_reply(self, msg):
234 """ Handles replies for code execution.
221 """ Handles replies for code execution.
235 """
222 """
236 info = self._request_info.get('execute')
223 info = self._request_info.get('execute')
237 if info and info.id == msg['parent_header']['msg_id'] and \
224 if info and info.id == msg['parent_header']['msg_id'] and \
238 not self._hidden:
225 info.kind == 'user' and not self._hidden:
239 # Make sure that all output from the SUB channel has been processed
226 # Make sure that all output from the SUB channel has been processed
240 # before writing a new prompt.
227 # before writing a new prompt.
241 self.kernel_manager.sub_channel.flush()
228 self.kernel_manager.sub_channel.flush()
242
229
243 content = msg['content']
230 content = msg['content']
244 status = content['status']
231 status = content['status']
245 if status == 'ok':
232 if status == 'ok':
246 self._process_execute_ok(msg)
233 self._process_execute_ok(msg)
247 elif status == 'error':
234 elif status == 'error':
248 self._process_execute_error(msg)
235 self._process_execute_error(msg)
249 elif status == 'abort':
236 elif status == 'abort':
250 self._process_execute_abort(msg)
237 self._process_execute_abort(msg)
251
238
252 self._show_interpreter_prompt_for_reply(msg)
239 self._show_interpreter_prompt_for_reply(msg)
253 self.executed.emit(msg)
240 self.executed.emit(msg)
254
241
255 def _handle_input_request(self, msg):
242 def _handle_input_request(self, msg):
256 """ Handle requests for raw_input.
243 """ Handle requests for raw_input.
257 """
244 """
258 if self._hidden:
245 if self._hidden:
259 raise RuntimeError('Request for raw input during hidden execution.')
246 raise RuntimeError('Request for raw input during hidden execution.')
260
247
261 # Make sure that all output from the SUB channel has been processed
248 # Make sure that all output from the SUB channel has been processed
262 # before entering readline mode.
249 # before entering readline mode.
263 self.kernel_manager.sub_channel.flush()
250 self.kernel_manager.sub_channel.flush()
264
251
265 def callback(line):
252 def callback(line):
266 self.kernel_manager.rep_channel.input(line)
253 self.kernel_manager.rep_channel.input(line)
267 self._readline(msg['content']['prompt'], callback=callback)
254 self._readline(msg['content']['prompt'], callback=callback)
268
255
269 def _handle_kernel_died(self, since_last_heartbeat):
256 def _handle_kernel_died(self, since_last_heartbeat):
270 """ Handle the kernel's death by asking if the user wants to restart.
257 """ Handle the kernel's death by asking if the user wants to restart.
271 """
258 """
272 message = 'The kernel heartbeat has been inactive for %.2f ' \
259 message = 'The kernel heartbeat has been inactive for %.2f ' \
273 'seconds. Do you want to restart the kernel? You may ' \
260 'seconds. Do you want to restart the kernel? You may ' \
274 'first want to check the network connection.' % \
261 'first want to check the network connection.' % \
275 since_last_heartbeat
262 since_last_heartbeat
276 if self.custom_restart:
263 if self.custom_restart:
277 self.custom_restart_kernel_died.emit(since_last_heartbeat)
264 self.custom_restart_kernel_died.emit(since_last_heartbeat)
278 else:
265 else:
279 self.restart_kernel(message)
266 self.restart_kernel(message)
280
267
281 def _handle_object_info_reply(self, rep):
268 def _handle_object_info_reply(self, rep):
282 """ Handle replies for call tips.
269 """ Handle replies for call tips.
283 """
270 """
284 cursor = self._get_cursor()
271 cursor = self._get_cursor()
285 info = self._request_info.get('call_tip')
272 info = self._request_info.get('call_tip')
286 if info and info.id == rep['parent_header']['msg_id'] and \
273 if info and info.id == rep['parent_header']['msg_id'] and \
287 info.pos == cursor.position():
274 info.pos == cursor.position():
288 doc = rep['content']['docstring']
275 doc = rep['content']['docstring']
289 if doc:
276 if doc:
290 self._call_tip_widget.show_docstring(doc)
277 self._call_tip_widget.show_docstring(doc)
291
278
292 def _handle_pyout(self, msg):
279 def _handle_pyout(self, msg):
293 """ Handle display hook output.
280 """ Handle display hook output.
294 """
281 """
295 if not self._hidden and self._is_from_this_session(msg):
282 if not self._hidden and self._is_from_this_session(msg):
296 self._append_plain_text(msg['content']['data'] + '\n')
283 self._append_plain_text(msg['content']['data'] + '\n')
297
284
298 def _handle_stream(self, msg):
285 def _handle_stream(self, msg):
299 """ Handle stdout, stderr, and stdin.
286 """ Handle stdout, stderr, and stdin.
300 """
287 """
301 if not self._hidden and self._is_from_this_session(msg):
288 if not self._hidden and self._is_from_this_session(msg):
302 self._append_plain_text(msg['content']['data'])
289 self._append_plain_text(msg['content']['data'])
303 self._control.moveCursor(QtGui.QTextCursor.End)
290 self._control.moveCursor(QtGui.QTextCursor.End)
304
291
305 def _started_channels(self):
292 def _started_channels(self):
306 """ Called when the KernelManager channels have started listening or
293 """ Called when the KernelManager channels have started listening or
307 when the frontend is assigned an already listening KernelManager.
294 when the frontend is assigned an already listening KernelManager.
308 """
295 """
309 self._control.clear()
296 self._control.clear()
310 self._append_plain_text(self._get_banner())
297 self._append_plain_text(self._get_banner())
311 self._show_interpreter_prompt()
298 self._show_interpreter_prompt()
312
299
313 def _stopped_channels(self):
300 def _stopped_channels(self):
314 """ Called when the KernelManager channels have stopped listening or
301 """ Called when the KernelManager channels have stopped listening or
315 when a listening KernelManager is removed from the frontend.
302 when a listening KernelManager is removed from the frontend.
316 """
303 """
317 self._executing = self._reading = False
304 self._executing = self._reading = False
318 self._highlighter.highlighting_on = False
305 self._highlighter.highlighting_on = False
319
306
320 #---------------------------------------------------------------------------
307 #---------------------------------------------------------------------------
321 # 'FrontendWidget' interface
308 # 'FrontendWidget' interface
322 #---------------------------------------------------------------------------
309 #---------------------------------------------------------------------------
323
310
324 def execute_file(self, path, hidden=False):
311 def execute_file(self, path, hidden=False):
325 """ Attempts to execute file with 'path'. If 'hidden', no output is
312 """ Attempts to execute file with 'path'. If 'hidden', no output is
326 shown.
313 shown.
327 """
314 """
328 self.execute('execfile("%s")' % path, hidden=hidden)
315 self.execute('execfile("%s")' % path, hidden=hidden)
329
316
330 def interrupt_kernel(self):
317 def interrupt_kernel(self):
331 """ Attempts to interrupt the running kernel.
318 """ Attempts to interrupt the running kernel.
332 """
319 """
333 if self.custom_interrupt:
320 if self.custom_interrupt:
334 self.custom_interrupt_requested.emit()
321 self.custom_interrupt_requested.emit()
335 elif self.kernel_manager.has_kernel:
322 elif self.kernel_manager.has_kernel:
336 self.kernel_manager.signal_kernel(signal.SIGINT)
323 self.kernel_manager.signal_kernel(signal.SIGINT)
337 else:
324 else:
338 self._append_plain_text('Kernel process is either remote or '
325 self._append_plain_text('Kernel process is either remote or '
339 'unspecified. Cannot interrupt.\n')
326 'unspecified. Cannot interrupt.\n')
340
327
341 def restart_kernel(self, message):
328 def restart_kernel(self, message):
342 """ Attempts to restart the running kernel.
329 """ Attempts to restart the running kernel.
343 """
330 """
344 # We want to make sure that if this dialog is already happening, that
331 # We want to make sure that if this dialog is already happening, that
345 # other signals don't trigger it again. This can happen when the
332 # other signals don't trigger it again. This can happen when the
346 # kernel_died heartbeat signal is emitted and the user is slow to
333 # kernel_died heartbeat signal is emitted and the user is slow to
347 # respond to the dialog.
334 # respond to the dialog.
348 if not self._possible_kernel_restart:
335 if not self._possible_kernel_restart:
349 if self.custom_restart:
336 if self.custom_restart:
350 self.custom_restart_requested.emit()
337 self.custom_restart_requested.emit()
351 elif self.kernel_manager.has_kernel:
338 elif self.kernel_manager.has_kernel:
352 # Setting this to True will prevent this logic from happening
339 # Setting this to True will prevent this logic from happening
353 # again until the current pass is completed.
340 # again until the current pass is completed.
354 self._possible_kernel_restart = True
341 self._possible_kernel_restart = True
355 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
342 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
356 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
343 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
357 message, buttons)
344 message, buttons)
358 if result == QtGui.QMessageBox.Yes:
345 if result == QtGui.QMessageBox.Yes:
359 try:
346 try:
360 self.kernel_manager.restart_kernel()
347 self.kernel_manager.restart_kernel()
361 except RuntimeError:
348 except RuntimeError:
362 message = 'Kernel started externally. Cannot restart.\n'
349 message = 'Kernel started externally. Cannot restart.\n'
363 self._append_plain_text(message)
350 self._append_plain_text(message)
364 else:
351 else:
365 self._stopped_channels()
352 self._stopped_channels()
366 self._append_plain_text('Kernel restarting...\n')
353 self._append_plain_text('Kernel restarting...\n')
367 self._show_interpreter_prompt()
354 self._show_interpreter_prompt()
368 # This might need to be moved to another location?
355 # This might need to be moved to another location?
369 self._possible_kernel_restart = False
356 self._possible_kernel_restart = False
370 else:
357 else:
371 self._append_plain_text('Kernel process is either remote or '
358 self._append_plain_text('Kernel process is either remote or '
372 'unspecified. Cannot restart.\n')
359 'unspecified. Cannot restart.\n')
373
360
374 def closeEvent(self, event):
375 reply = QtGui.QMessageBox.question(self, 'Python',
376 'Close console?', QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
377 if reply == QtGui.QMessageBox.Yes:
378 self.shutdown_kernel()
379 event.accept()
380 else:
381 event.ignore()
382
383 # Move elsewhere to a better location later, possibly rename
384 def shutdown_kernel(self):
385 # Send quit message to kernel. Once we implement kernel-side setattr,
386 # this should probably be done that way, but for now this will do.
387 self.kernel_manager.xreq_channel.execute(
388 'get_ipython().exit_now=True', silent=True)
389
390 # Don't send any additional kernel kill messages immediately, to give
391 # the kernel a chance to properly execute shutdown actions.
392 # Wait for at most 2s, check every 0.1s.
393 for i in range(20):
394 if self.kernel_manager.is_alive:
395 time.sleep(0.1)
396 else:
397 break
398 else:
399 # OK, we've waited long enough.
400 self.kernel_manager.kill_kernel()
401
402 # FIXME: This logic may not be quite right... Perhaps the quit call is
403 # made elsewhere by Qt...
404 QtGui.QApplication.quit()
405
406 #---------------------------------------------------------------------------
361 #---------------------------------------------------------------------------
407 # 'FrontendWidget' protected interface
362 # 'FrontendWidget' protected interface
408 #---------------------------------------------------------------------------
363 #---------------------------------------------------------------------------
409
364
410 def _call_tip(self):
365 def _call_tip(self):
411 """ Shows a call tip, if appropriate, at the current cursor location.
366 """ Shows a call tip, if appropriate, at the current cursor location.
412 """
367 """
413 # Decide if it makes sense to show a call tip
368 # Decide if it makes sense to show a call tip
414 cursor = self._get_cursor()
369 cursor = self._get_cursor()
415 cursor.movePosition(QtGui.QTextCursor.Left)
370 cursor.movePosition(QtGui.QTextCursor.Left)
416 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
371 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
417 return False
372 return False
418 context = self._get_context(cursor)
373 context = self._get_context(cursor)
419 if not context:
374 if not context:
420 return False
375 return False
421
376
422 # Send the metadata request to the kernel
377 # Send the metadata request to the kernel
423 name = '.'.join(context)
378 name = '.'.join(context)
424 msg_id = self.kernel_manager.xreq_channel.object_info(name)
379 msg_id = self.kernel_manager.xreq_channel.object_info(name)
425 pos = self._get_cursor().position()
380 pos = self._get_cursor().position()
426 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
381 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
427 return True
382 return True
428
383
429 def _complete(self):
384 def _complete(self):
430 """ Performs completion at the current cursor location.
385 """ Performs completion at the current cursor location.
431 """
386 """
432 context = self._get_context()
387 context = self._get_context()
433 if context:
388 if context:
434 # Send the completion request to the kernel
389 # Send the completion request to the kernel
435 msg_id = self.kernel_manager.xreq_channel.complete(
390 msg_id = self.kernel_manager.xreq_channel.complete(
436 '.'.join(context), # text
391 '.'.join(context), # text
437 self._get_input_buffer_cursor_line(), # line
392 self._get_input_buffer_cursor_line(), # line
438 self._get_input_buffer_cursor_column(), # cursor_pos
393 self._get_input_buffer_cursor_column(), # cursor_pos
439 self.input_buffer) # block
394 self.input_buffer) # block
440 pos = self._get_cursor().position()
395 pos = self._get_cursor().position()
441 info = self._CompletionRequest(msg_id, pos)
396 info = self._CompletionRequest(msg_id, pos)
442 self._request_info['complete'] = info
397 self._request_info['complete'] = info
443
398
444 def _get_banner(self):
399 def _get_banner(self):
445 """ Gets a banner to display at the beginning of a session.
400 """ Gets a banner to display at the beginning of a session.
446 """
401 """
447 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
402 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
448 '"license" for more information.'
403 '"license" for more information.'
449 return banner % (sys.version, sys.platform)
404 return banner % (sys.version, sys.platform)
450
405
451 def _get_context(self, cursor=None):
406 def _get_context(self, cursor=None):
452 """ Gets the context for the specified cursor (or the current cursor
407 """ Gets the context for the specified cursor (or the current cursor
453 if none is specified).
408 if none is specified).
454 """
409 """
455 if cursor is None:
410 if cursor is None:
456 cursor = self._get_cursor()
411 cursor = self._get_cursor()
457 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
412 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
458 QtGui.QTextCursor.KeepAnchor)
413 QtGui.QTextCursor.KeepAnchor)
459 text = str(cursor.selection().toPlainText())
414 text = str(cursor.selection().toPlainText())
460 return self._completion_lexer.get_context(text)
415 return self._completion_lexer.get_context(text)
461
416
462 def _process_execute_abort(self, msg):
417 def _process_execute_abort(self, msg):
463 """ Process a reply for an aborted execution request.
418 """ Process a reply for an aborted execution request.
464 """
419 """
465 self._append_plain_text("ERROR: execution aborted\n")
420 self._append_plain_text("ERROR: execution aborted\n")
466
421
467 def _process_execute_error(self, msg):
422 def _process_execute_error(self, msg):
468 """ Process a reply for an execution request that resulted in an error.
423 """ Process a reply for an execution request that resulted in an error.
469 """
424 """
470 content = msg['content']
425 content = msg['content']
471 traceback = ''.join(content['traceback'])
426 traceback = ''.join(content['traceback'])
472 self._append_plain_text(traceback)
427 self._append_plain_text(traceback)
473
428
474 def _process_execute_ok(self, msg):
429 def _process_execute_ok(self, msg):
475 """ Process a reply for a successful execution equest.
430 """ Process a reply for a successful execution equest.
476 """
431 """
477 payload = msg['content']['payload']
432 payload = msg['content']['payload']
478 for item in payload:
433 for item in payload:
479 if not self._process_execute_payload(item):
434 if not self._process_execute_payload(item):
480 warning = 'Received unknown payload of type %s\n'
435 warning = 'Received unknown payload of type %s\n'
481 self._append_plain_text(warning % repr(item['source']))
436 self._append_plain_text(warning % repr(item['source']))
482
437
483 def _process_execute_payload(self, item):
438 def _process_execute_payload(self, item):
484 """ Process a single payload item from the list of payload items in an
439 """ Process a single payload item from the list of payload items in an
485 execution reply. Returns whether the payload was handled.
440 execution reply. Returns whether the payload was handled.
486 """
441 """
487 # The basic FrontendWidget doesn't handle payloads, as they are a
442 # The basic FrontendWidget doesn't handle payloads, as they are a
488 # mechanism for going beyond the standard Python interpreter model.
443 # mechanism for going beyond the standard Python interpreter model.
489 return False
444 return False
490
445
491 def _show_interpreter_prompt(self):
446 def _show_interpreter_prompt(self):
492 """ Shows a prompt for the interpreter.
447 """ Shows a prompt for the interpreter.
493 """
448 """
494 self._show_prompt('>>> ')
449 self._show_prompt('>>> ')
495
450
496 def _show_interpreter_prompt_for_reply(self, msg):
451 def _show_interpreter_prompt_for_reply(self, msg):
497 """ Shows a prompt for the interpreter given an 'execute_reply' message.
452 """ Shows a prompt for the interpreter given an 'execute_reply' message.
498 """
453 """
499 self._show_interpreter_prompt()
454 self._show_interpreter_prompt()
500
455
501 #------ Signal handlers ----------------------------------------------------
456 #------ Signal handlers ----------------------------------------------------
502
457
503 def _document_contents_change(self, position, removed, added):
458 def _document_contents_change(self, position, removed, added):
504 """ Called whenever the document's content changes. Display a call tip
459 """ Called whenever the document's content changes. Display a call tip
505 if appropriate.
460 if appropriate.
506 """
461 """
507 # Calculate where the cursor should be *after* the change:
462 # Calculate where the cursor should be *after* the change:
508 position += added
463 position += added
509
464
510 document = self._control.document()
465 document = self._control.document()
511 if position == self._get_cursor().position():
466 if position == self._get_cursor().position():
512 self._call_tip()
467 self._call_tip()
@@ -1,436 +1,438 b''
1 """ A FrontendWidget that emulates the interface of the console IPython and
1 """ A FrontendWidget that emulates the interface of the console IPython and
2 supports the additional functionality provided by the IPython kernel.
2 supports the additional functionality provided by the IPython kernel.
3
3
4 TODO: Add support for retrieving the system default editor. Requires code
4 TODO: Add support for retrieving the system default editor. Requires code
5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 Linux (use the xdg system).
6 Linux (use the xdg system).
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Imports
10 # Imports
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 # Standard library imports
13 # Standard library imports
14 from collections import namedtuple
14 from collections import namedtuple
15 import re
15 import re
16 from subprocess import Popen
16 from subprocess import Popen
17
17
18 # System library imports
18 # System library imports
19 from PyQt4 import QtCore, QtGui
19 from PyQt4 import QtCore, QtGui
20
20
21 # Local imports
21 # Local imports
22 from IPython.core.inputsplitter import IPythonInputSplitter
22 from IPython.core.inputsplitter import IPythonInputSplitter
23 from IPython.core.usage import default_banner
23 from IPython.core.usage import default_banner
24 from IPython.utils.traitlets import Bool, Str
24 from IPython.utils.traitlets import Bool, Str
25 from frontend_widget import FrontendWidget
25 from frontend_widget import FrontendWidget
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Constants
28 # Constants
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31 # The default light style sheet: black text on a white background.
31 # The default light style sheet: black text on a white background.
32 default_light_style_sheet = '''
32 default_light_style_sheet = '''
33 .error { color: red; }
33 .error { color: red; }
34 .in-prompt { color: navy; }
34 .in-prompt { color: navy; }
35 .in-prompt-number { font-weight: bold; }
35 .in-prompt-number { font-weight: bold; }
36 .out-prompt { color: darkred; }
36 .out-prompt { color: darkred; }
37 .out-prompt-number { font-weight: bold; }
37 .out-prompt-number { font-weight: bold; }
38 '''
38 '''
39 default_light_syntax_style = 'default'
39 default_light_syntax_style = 'default'
40
40
41 # The default dark style sheet: white text on a black background.
41 # The default dark style sheet: white text on a black background.
42 default_dark_style_sheet = '''
42 default_dark_style_sheet = '''
43 QPlainTextEdit, QTextEdit { background-color: black; color: white }
43 QPlainTextEdit, QTextEdit { background-color: black; color: white }
44 QFrame { border: 1px solid grey; }
44 QFrame { border: 1px solid grey; }
45 .error { color: red; }
45 .error { color: red; }
46 .in-prompt { color: lime; }
46 .in-prompt { color: lime; }
47 .in-prompt-number { color: lime; font-weight: bold; }
47 .in-prompt-number { color: lime; font-weight: bold; }
48 .out-prompt { color: red; }
48 .out-prompt { color: red; }
49 .out-prompt-number { color: red; font-weight: bold; }
49 .out-prompt-number { color: red; font-weight: bold; }
50 '''
50 '''
51 default_dark_syntax_style = 'monokai'
51 default_dark_syntax_style = 'monokai'
52
52
53 # Default strings to build and display input and output prompts (and separators
53 # Default strings to build and display input and output prompts (and separators
54 # in between)
54 # in between)
55 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
55 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
56 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
56 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
57 default_input_sep = '\n'
57 default_input_sep = '\n'
58 default_output_sep = ''
58 default_output_sep = ''
59 default_output_sep2 = ''
59 default_output_sep2 = ''
60
60
61 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
62 # IPythonWidget class
62 # IPythonWidget class
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64
64
65 class IPythonWidget(FrontendWidget):
65 class IPythonWidget(FrontendWidget):
66 """ A FrontendWidget for an IPython kernel.
66 """ A FrontendWidget for an IPython kernel.
67 """
67 """
68
68
69 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
69 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
70 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
70 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
71 # settings.
71 # settings.
72 custom_edit = Bool(False)
72 custom_edit = Bool(False)
73 custom_edit_requested = QtCore.pyqtSignal(object, object)
73 custom_edit_requested = QtCore.pyqtSignal(object, object)
74
74
75 # A command for invoking a system text editor. If the string contains a
75 # A command for invoking a system text editor. If the string contains a
76 # {filename} format specifier, it will be used. Otherwise, the filename will
76 # {filename} format specifier, it will be used. Otherwise, the filename will
77 # be appended to the end the command.
77 # be appended to the end the command.
78 editor = Str('default', config=True)
78 editor = Str('default', config=True)
79
79
80 # The editor command to use when a specific line number is requested. The
80 # The editor command to use when a specific line number is requested. The
81 # string should contain two format specifiers: {line} and {filename}. If
81 # string should contain two format specifiers: {line} and {filename}. If
82 # this parameter is not specified, the line number option to the %edit magic
82 # this parameter is not specified, the line number option to the %edit magic
83 # will be ignored.
83 # will be ignored.
84 editor_line = Str(config=True)
84 editor_line = Str(config=True)
85
85
86 # A CSS stylesheet. The stylesheet can contain classes for:
86 # A CSS stylesheet. The stylesheet can contain classes for:
87 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
87 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
88 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
88 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
89 # 3. IPython: .error, .in-prompt, .out-prompt, etc
89 # 3. IPython: .error, .in-prompt, .out-prompt, etc
90 style_sheet = Str(config=True)
90 style_sheet = Str(config=True)
91
91
92 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
92 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
93 # the style sheet is queried for Pygments style information.
93 # the style sheet is queried for Pygments style information.
94 syntax_style = Str(config=True)
94 syntax_style = Str(config=True)
95
95
96 # Prompts.
96 # Prompts.
97 in_prompt = Str(default_in_prompt, config=True)
97 in_prompt = Str(default_in_prompt, config=True)
98 out_prompt = Str(default_out_prompt, config=True)
98 out_prompt = Str(default_out_prompt, config=True)
99 input_sep = Str(default_input_sep, config=True)
99 input_sep = Str(default_input_sep, config=True)
100 output_sep = Str(default_output_sep, config=True)
100 output_sep = Str(default_output_sep, config=True)
101 output_sep2 = Str(default_output_sep2, config=True)
101 output_sep2 = Str(default_output_sep2, config=True)
102
102
103 # FrontendWidget protected class variables.
103 # FrontendWidget protected class variables.
104 _input_splitter_class = IPythonInputSplitter
104 _input_splitter_class = IPythonInputSplitter
105
105
106 # IPythonWidget protected class variables.
106 # IPythonWidget protected class variables.
107 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
107 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
108 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
108 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
109 _payload_source_exit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.ask_exit'
109 _payload_source_exit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.ask_exit'
110 _payload_source_page = 'IPython.zmq.page.page'
110 _payload_source_page = 'IPython.zmq.page.page'
111
111
112 #---------------------------------------------------------------------------
112 #---------------------------------------------------------------------------
113 # 'object' interface
113 # 'object' interface
114 #---------------------------------------------------------------------------
114 #---------------------------------------------------------------------------
115
115
116 def __init__(self, *args, **kw):
116 def __init__(self, *args, **kw):
117 super(IPythonWidget, self).__init__(*args, **kw)
117 super(IPythonWidget, self).__init__(*args, **kw)
118
118
119 # IPythonWidget protected variables.
119 # IPythonWidget protected variables.
120 self._previous_prompt_obj = None
120 self._payload_handlers = {
121 self._payload_handlers = {
122 self._payload_source_edit : self._handle_payload_edit,
121 self._payload_source_edit : self._handle_payload_edit,
123 self._payload_source_exit : self._handle_payload_exit,
122 self._payload_source_exit : self._handle_payload_exit,
124 self._payload_source_page : self._handle_payload_page,
123 self._payload_source_page : self._handle_payload_page }
125 }
124 self._previous_prompt_obj = None
126
125
127 # Initialize widget styling.
126 # Initialize widget styling.
128 if self.style_sheet:
127 if self.style_sheet:
129 self._style_sheet_changed()
128 self._style_sheet_changed()
130 self._syntax_style_changed()
129 self._syntax_style_changed()
131 else:
130 else:
132 self.set_default_style()
131 self.set_default_style()
133
132
134 #---------------------------------------------------------------------------
133 #---------------------------------------------------------------------------
135 # 'BaseFrontendMixin' abstract interface
134 # 'BaseFrontendMixin' abstract interface
136 #---------------------------------------------------------------------------
135 #---------------------------------------------------------------------------
137
136
138 def _handle_complete_reply(self, rep):
137 def _handle_complete_reply(self, rep):
139 """ Reimplemented to support IPython's improved completion machinery.
138 """ Reimplemented to support IPython's improved completion machinery.
140 """
139 """
141 cursor = self._get_cursor()
140 cursor = self._get_cursor()
142 info = self._request_info.get('complete')
141 info = self._request_info.get('complete')
143 if info and info.id == rep['parent_header']['msg_id'] and \
142 if info and info.id == rep['parent_header']['msg_id'] and \
144 info.pos == cursor.position():
143 info.pos == cursor.position():
145 matches = rep['content']['matches']
144 matches = rep['content']['matches']
146 text = rep['content']['matched_text']
145 text = rep['content']['matched_text']
147 offset = len(text)
146 offset = len(text)
148
147
149 # Clean up matches with period and path separators if the matched
148 # Clean up matches with period and path separators if the matched
150 # text has not been transformed. This is done by truncating all
149 # text has not been transformed. This is done by truncating all
151 # but the last component and then suitably decreasing the offset
150 # but the last component and then suitably decreasing the offset
152 # between the current cursor position and the start of completion.
151 # between the current cursor position and the start of completion.
153 if len(matches) > 1 and matches[0][:offset] == text:
152 if len(matches) > 1 and matches[0][:offset] == text:
154 parts = re.split(r'[./\\]', text)
153 parts = re.split(r'[./\\]', text)
155 sep_count = len(parts) - 1
154 sep_count = len(parts) - 1
156 if sep_count:
155 if sep_count:
157 chop_length = sum(map(len, parts[:sep_count])) + sep_count
156 chop_length = sum(map(len, parts[:sep_count])) + sep_count
158 matches = [ match[chop_length:] for match in matches ]
157 matches = [ match[chop_length:] for match in matches ]
159 offset -= chop_length
158 offset -= chop_length
160
159
161 # Move the cursor to the start of the match and complete.
160 # Move the cursor to the start of the match and complete.
162 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
161 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
163 self._complete_with_items(cursor, matches)
162 self._complete_with_items(cursor, matches)
164
163
165 def _handle_execute_reply(self, msg):
164 def _handle_execute_reply(self, msg):
166 """ Reimplemented to support prompt requests.
165 """ Reimplemented to support prompt requests.
167 """
166 """
168 info = self._request_info.get('execute')
167 info = self._request_info.get('execute')
169 if info and info.id == msg['parent_header']['msg_id']:
168 if info and info.id == msg['parent_header']['msg_id']:
170 if info.kind == 'prompt':
169 if info.kind == 'prompt':
171 number = msg['content']['execution_count'] + 1
170 number = msg['content']['execution_count'] + 1
172 self._show_interpreter_prompt(number)
171 self._show_interpreter_prompt(number)
173 else:
172 else:
174 super(IPythonWidget, self)._handle_execute_reply(msg)
173 super(IPythonWidget, self)._handle_execute_reply(msg)
175
174
176 def _handle_history_reply(self, msg):
175 def _handle_history_reply(self, msg):
177 """ Implemented to handle history replies, which are only supported by
176 """ Implemented to handle history replies, which are only supported by
178 the IPython kernel.
177 the IPython kernel.
179 """
178 """
180 history_dict = msg['content']['history']
179 history_dict = msg['content']['history']
181 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
180 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
182 self._set_history(items)
181 self._set_history(items)
183
182
184 def _handle_pyout(self, msg):
183 def _handle_pyout(self, msg):
185 """ Reimplemented for IPython-style "display hook".
184 """ Reimplemented for IPython-style "display hook".
186 """
185 """
187 if not self._hidden and self._is_from_this_session(msg):
186 if not self._hidden and self._is_from_this_session(msg):
188 content = msg['content']
187 content = msg['content']
189 prompt_number = content['execution_count']
188 prompt_number = content['execution_count']
190 self._append_plain_text(self.output_sep)
189 self._append_plain_text(self.output_sep)
191 self._append_html(self._make_out_prompt(prompt_number))
190 self._append_html(self._make_out_prompt(prompt_number))
192 self._append_plain_text(content['data']+self.output_sep2)
191 self._append_plain_text(content['data']+self.output_sep2)
193
192
194 def _started_channels(self):
193 def _started_channels(self):
195 """ Reimplemented to make a history request.
194 """ Reimplemented to make a history request.
196 """
195 """
197 super(IPythonWidget, self)._started_channels()
196 super(IPythonWidget, self)._started_channels()
198 # FIXME: Disabled until history requests are properly implemented.
197 # FIXME: Disabled until history requests are properly implemented.
199 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
198 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
200
199
201 #---------------------------------------------------------------------------
200 #---------------------------------------------------------------------------
202 # 'FrontendWidget' interface
201 # 'FrontendWidget' interface
203 #---------------------------------------------------------------------------
202 #---------------------------------------------------------------------------
204
203
205 def execute_file(self, path, hidden=False):
204 def execute_file(self, path, hidden=False):
206 """ Reimplemented to use the 'run' magic.
205 """ Reimplemented to use the 'run' magic.
207 """
206 """
208 self.execute('%%run %s' % path, hidden=hidden)
207 self.execute('%%run %s' % path, hidden=hidden)
209
208
210 #---------------------------------------------------------------------------
209 #---------------------------------------------------------------------------
211 # 'FrontendWidget' protected interface
210 # 'FrontendWidget' protected interface
212 #---------------------------------------------------------------------------
211 #---------------------------------------------------------------------------
213
212
214 def _complete(self):
213 def _complete(self):
215 """ Reimplemented to support IPython's improved completion machinery.
214 """ Reimplemented to support IPython's improved completion machinery.
216 """
215 """
217 # We let the kernel split the input line, so we *always* send an empty
216 # We let the kernel split the input line, so we *always* send an empty
218 # text field. Readline-based frontends do get a real text field which
217 # text field. Readline-based frontends do get a real text field which
219 # they can use.
218 # they can use.
220 text = ''
219 text = ''
221
220
222 # Send the completion request to the kernel
221 # Send the completion request to the kernel
223 msg_id = self.kernel_manager.xreq_channel.complete(
222 msg_id = self.kernel_manager.xreq_channel.complete(
224 text, # text
223 text, # text
225 self._get_input_buffer_cursor_line(), # line
224 self._get_input_buffer_cursor_line(), # line
226 self._get_input_buffer_cursor_column(), # cursor_pos
225 self._get_input_buffer_cursor_column(), # cursor_pos
227 self.input_buffer) # block
226 self.input_buffer) # block
228 pos = self._get_cursor().position()
227 pos = self._get_cursor().position()
229 info = self._CompletionRequest(msg_id, pos)
228 info = self._CompletionRequest(msg_id, pos)
230 self._request_info['complete'] = info
229 self._request_info['complete'] = info
231
230
232 def _get_banner(self):
231 def _get_banner(self):
233 """ Reimplemented to return IPython's default banner.
232 """ Reimplemented to return IPython's default banner.
234 """
233 """
235 return default_banner + '\n'
234 return default_banner + '\n'
236
235
237 def _process_execute_error(self, msg):
236 def _process_execute_error(self, msg):
238 """ Reimplemented for IPython-style traceback formatting.
237 """ Reimplemented for IPython-style traceback formatting.
239 """
238 """
240 content = msg['content']
239 content = msg['content']
241 traceback = '\n'.join(content['traceback']) + '\n'
240 traceback = '\n'.join(content['traceback']) + '\n'
242 if False:
241 if False:
243 # FIXME: For now, tracebacks come as plain text, so we can't use
242 # FIXME: For now, tracebacks come as plain text, so we can't use
244 # the html renderer yet. Once we refactor ultratb to produce
243 # the html renderer yet. Once we refactor ultratb to produce
245 # properly styled tracebacks, this branch should be the default
244 # properly styled tracebacks, this branch should be the default
246 traceback = traceback.replace(' ', '&nbsp;')
245 traceback = traceback.replace(' ', '&nbsp;')
247 traceback = traceback.replace('\n', '<br/>')
246 traceback = traceback.replace('\n', '<br/>')
248
247
249 ename = content['ename']
248 ename = content['ename']
250 ename_styled = '<span class="error">%s</span>' % ename
249 ename_styled = '<span class="error">%s</span>' % ename
251 traceback = traceback.replace(ename, ename_styled)
250 traceback = traceback.replace(ename, ename_styled)
252
251
253 self._append_html(traceback)
252 self._append_html(traceback)
254 else:
253 else:
255 # This is the fallback for now, using plain text with ansi escapes
254 # This is the fallback for now, using plain text with ansi escapes
256 self._append_plain_text(traceback)
255 self._append_plain_text(traceback)
257
258 # Payload handlers with generic interface: each takes the opaque payload
259 # dict, unpacks it and calls the underlying functions with the necessary
260 # arguments
261 def _handle_payload_edit(self, item):
262 self._edit(item['filename'], item['line_number'])
263
264 def _handle_payload_exit(self, item):
265 QtCore.QCoreApplication.postEvent(self, QtGui.QCloseEvent())
266
267 def _handle_payload_page(self, item):
268 self._page(item['data'])
269
256
270 def _process_execute_payload(self, item):
257 def _process_execute_payload(self, item):
271 """ Reimplemented to handle %edit and paging payloads.
258 """ Reimplemented to dispatch payloads to handler methods.
272 """
259 """
273 handler = self._payload_handlers.get(item['source'])
260 handler = self._payload_handlers.get(item['source'])
274 if handler is None:
261 if handler is None:
275 # We have no handler for this type of payload, simply ignore it
262 # We have no handler for this type of payload, simply ignore it
276 return False
263 return False
277 else:
264 else:
278 handler(item)
265 handler(item)
279 return True
266 return True
280
267
281 def _show_interpreter_prompt(self, number=None):
268 def _show_interpreter_prompt(self, number=None):
282 """ Reimplemented for IPython-style prompts.
269 """ Reimplemented for IPython-style prompts.
283 """
270 """
284 # If a number was not specified, make a prompt number request.
271 # If a number was not specified, make a prompt number request.
285 if number is None:
272 if number is None:
286 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
273 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
287 info = self._ExecutionRequest(msg_id, 'prompt')
274 info = self._ExecutionRequest(msg_id, 'prompt')
288 self._request_info['execute'] = info
275 self._request_info['execute'] = info
289 return
276 return
290
277
291 # Show a new prompt and save information about it so that it can be
278 # Show a new prompt and save information about it so that it can be
292 # updated later if the prompt number turns out to be wrong.
279 # updated later if the prompt number turns out to be wrong.
293 self._prompt_sep = self.input_sep
280 self._prompt_sep = self.input_sep
294 self._show_prompt(self._make_in_prompt(number), html=True)
281 self._show_prompt(self._make_in_prompt(number), html=True)
295 block = self._control.document().lastBlock()
282 block = self._control.document().lastBlock()
296 length = len(self._prompt)
283 length = len(self._prompt)
297 self._previous_prompt_obj = self._PromptBlock(block, length, number)
284 self._previous_prompt_obj = self._PromptBlock(block, length, number)
298
285
299 # Update continuation prompt to reflect (possibly) new prompt length.
286 # Update continuation prompt to reflect (possibly) new prompt length.
300 self._set_continuation_prompt(
287 self._set_continuation_prompt(
301 self._make_continuation_prompt(self._prompt), html=True)
288 self._make_continuation_prompt(self._prompt), html=True)
302
289
303 def _show_interpreter_prompt_for_reply(self, msg):
290 def _show_interpreter_prompt_for_reply(self, msg):
304 """ Reimplemented for IPython-style prompts.
291 """ Reimplemented for IPython-style prompts.
305 """
292 """
306 # Update the old prompt number if necessary.
293 # Update the old prompt number if necessary.
307 content = msg['content']
294 content = msg['content']
308 previous_prompt_number = content['execution_count']
295 previous_prompt_number = content['execution_count']
309 if self._previous_prompt_obj and \
296 if self._previous_prompt_obj and \
310 self._previous_prompt_obj.number != previous_prompt_number:
297 self._previous_prompt_obj.number != previous_prompt_number:
311 block = self._previous_prompt_obj.block
298 block = self._previous_prompt_obj.block
312
299
313 # Make sure the prompt block has not been erased.
300 # Make sure the prompt block has not been erased.
314 if block.isValid() and not block.text().isEmpty():
301 if block.isValid() and not block.text().isEmpty():
315
302
316 # Remove the old prompt and insert a new prompt.
303 # Remove the old prompt and insert a new prompt.
317 cursor = QtGui.QTextCursor(block)
304 cursor = QtGui.QTextCursor(block)
318 cursor.movePosition(QtGui.QTextCursor.Right,
305 cursor.movePosition(QtGui.QTextCursor.Right,
319 QtGui.QTextCursor.KeepAnchor,
306 QtGui.QTextCursor.KeepAnchor,
320 self._previous_prompt_obj.length)
307 self._previous_prompt_obj.length)
321 prompt = self._make_in_prompt(previous_prompt_number)
308 prompt = self._make_in_prompt(previous_prompt_number)
322 self._prompt = self._insert_html_fetching_plain_text(
309 self._prompt = self._insert_html_fetching_plain_text(
323 cursor, prompt)
310 cursor, prompt)
324
311
325 # When the HTML is inserted, Qt blows away the syntax
312 # When the HTML is inserted, Qt blows away the syntax
326 # highlighting for the line, so we need to rehighlight it.
313 # highlighting for the line, so we need to rehighlight it.
327 self._highlighter.rehighlightBlock(cursor.block())
314 self._highlighter.rehighlightBlock(cursor.block())
328
315
329 self._previous_prompt_obj = None
316 self._previous_prompt_obj = None
330
317
331 # Show a new prompt with the kernel's estimated prompt number.
318 # Show a new prompt with the kernel's estimated prompt number.
332 self._show_interpreter_prompt(previous_prompt_number+1)
319 self._show_interpreter_prompt(previous_prompt_number+1)
333
320
334 #---------------------------------------------------------------------------
321 #---------------------------------------------------------------------------
335 # 'IPythonWidget' interface
322 # 'IPythonWidget' interface
336 #---------------------------------------------------------------------------
323 #---------------------------------------------------------------------------
337
324
338 def set_default_style(self, lightbg=True):
325 def set_default_style(self, lightbg=True):
339 """ Sets the widget style to the class defaults.
326 """ Sets the widget style to the class defaults.
340
327
341 Parameters:
328 Parameters:
342 -----------
329 -----------
343 lightbg : bool, optional (default True)
330 lightbg : bool, optional (default True)
344 Whether to use the default IPython light background or dark
331 Whether to use the default IPython light background or dark
345 background style.
332 background style.
346 """
333 """
347 if lightbg:
334 if lightbg:
348 self.style_sheet = default_light_style_sheet
335 self.style_sheet = default_light_style_sheet
349 self.syntax_style = default_light_syntax_style
336 self.syntax_style = default_light_syntax_style
350 else:
337 else:
351 self.style_sheet = default_dark_style_sheet
338 self.style_sheet = default_dark_style_sheet
352 self.syntax_style = default_dark_syntax_style
339 self.syntax_style = default_dark_syntax_style
353
340
354 #---------------------------------------------------------------------------
341 #---------------------------------------------------------------------------
355 # 'IPythonWidget' protected interface
342 # 'IPythonWidget' protected interface
356 #---------------------------------------------------------------------------
343 #---------------------------------------------------------------------------
357
344
358 def _edit(self, filename, line=None):
345 def _edit(self, filename, line=None):
359 """ Opens a Python script for editing.
346 """ Opens a Python script for editing.
360
347
361 Parameters:
348 Parameters:
362 -----------
349 -----------
363 filename : str
350 filename : str
364 A path to a local system file.
351 A path to a local system file.
365
352
366 line : int, optional
353 line : int, optional
367 A line of interest in the file.
354 A line of interest in the file.
368 """
355 """
369 if self.custom_edit:
356 if self.custom_edit:
370 self.custom_edit_requested.emit(filename, line)
357 self.custom_edit_requested.emit(filename, line)
371 elif self.editor == 'default':
358 elif self.editor == 'default':
372 self._append_plain_text('No default editor available.\n')
359 self._append_plain_text('No default editor available.\n')
373 else:
360 else:
374 try:
361 try:
375 filename = '"%s"' % filename
362 filename = '"%s"' % filename
376 if line and self.editor_line:
363 if line and self.editor_line:
377 command = self.editor_line.format(filename=filename,
364 command = self.editor_line.format(filename=filename,
378 line=line)
365 line=line)
379 else:
366 else:
380 try:
367 try:
381 command = self.editor.format()
368 command = self.editor.format()
382 except KeyError:
369 except KeyError:
383 command = self.editor.format(filename=filename)
370 command = self.editor.format(filename=filename)
384 else:
371 else:
385 command += ' ' + filename
372 command += ' ' + filename
386 except KeyError:
373 except KeyError:
387 self._append_plain_text('Invalid editor command.\n')
374 self._append_plain_text('Invalid editor command.\n')
388 else:
375 else:
389 try:
376 try:
390 Popen(command, shell=True)
377 Popen(command, shell=True)
391 except OSError:
378 except OSError:
392 msg = 'Opening editor with command "%s" failed.\n'
379 msg = 'Opening editor with command "%s" failed.\n'
393 self._append_plain_text(msg % command)
380 self._append_plain_text(msg % command)
394
381
395 def _make_in_prompt(self, number):
382 def _make_in_prompt(self, number):
396 """ Given a prompt number, returns an HTML In prompt.
383 """ Given a prompt number, returns an HTML In prompt.
397 """
384 """
398 body = self.in_prompt % number
385 body = self.in_prompt % number
399 return '<span class="in-prompt">%s</span>' % body
386 return '<span class="in-prompt">%s</span>' % body
400
387
401 def _make_continuation_prompt(self, prompt):
388 def _make_continuation_prompt(self, prompt):
402 """ Given a plain text version of an In prompt, returns an HTML
389 """ Given a plain text version of an In prompt, returns an HTML
403 continuation prompt.
390 continuation prompt.
404 """
391 """
405 end_chars = '...: '
392 end_chars = '...: '
406 space_count = len(prompt.lstrip('\n')) - len(end_chars)
393 space_count = len(prompt.lstrip('\n')) - len(end_chars)
407 body = '&nbsp;' * space_count + end_chars
394 body = '&nbsp;' * space_count + end_chars
408 return '<span class="in-prompt">%s</span>' % body
395 return '<span class="in-prompt">%s</span>' % body
409
396
410 def _make_out_prompt(self, number):
397 def _make_out_prompt(self, number):
411 """ Given a prompt number, returns an HTML Out prompt.
398 """ Given a prompt number, returns an HTML Out prompt.
412 """
399 """
413 body = self.out_prompt % number
400 body = self.out_prompt % number
414 return '<span class="out-prompt">%s</span>' % body
401 return '<span class="out-prompt">%s</span>' % body
415
402
403 #------ Payload handlers --------------------------------------------------
404
405 # Payload handlers with a generic interface: each takes the opaque payload
406 # dict, unpacks it and calls the underlying functions with the necessary
407 # arguments.
408
409 def _handle_payload_edit(self, item):
410 self._edit(item['filename'], item['line_number'])
411
412 def _handle_payload_exit(self, item):
413 self.exit_requested.emit()
414
415 def _handle_payload_page(self, item):
416 self._page(item['data'])
417
416 #------ Trait change handlers ---------------------------------------------
418 #------ Trait change handlers ---------------------------------------------
417
419
418 def _style_sheet_changed(self):
420 def _style_sheet_changed(self):
419 """ Set the style sheets of the underlying widgets.
421 """ Set the style sheets of the underlying widgets.
420 """
422 """
421 self.setStyleSheet(self.style_sheet)
423 self.setStyleSheet(self.style_sheet)
422 self._control.document().setDefaultStyleSheet(self.style_sheet)
424 self._control.document().setDefaultStyleSheet(self.style_sheet)
423 if self._page_control:
425 if self._page_control:
424 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
426 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
425
427
426 bg_color = self._control.palette().background().color()
428 bg_color = self._control.palette().background().color()
427 self._ansi_processor.set_background_color(bg_color)
429 self._ansi_processor.set_background_color(bg_color)
428
430
429 def _syntax_style_changed(self):
431 def _syntax_style_changed(self):
430 """ Set the style for the syntax highlighter.
432 """ Set the style for the syntax highlighter.
431 """
433 """
432 if self.syntax_style:
434 if self.syntax_style:
433 self._highlighter.set_style(self.syntax_style)
435 self._highlighter.set_style(self.syntax_style)
434 else:
436 else:
435 self._highlighter.set_style_sheet(self.style_sheet)
437 self._highlighter.set_style_sheet(self.style_sheet)
436
438
@@ -1,100 +1,146 b''
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2 """
2 """
3
3
4 #-----------------------------------------------------------------------------
5 # Imports
6 #-----------------------------------------------------------------------------
7
4 # Systemm library imports
8 # Systemm library imports
5 from PyQt4 import QtGui
9 from PyQt4 import QtGui
6
10
7 # Local imports
11 # Local imports
8 from IPython.external.argparse import ArgumentParser
12 from IPython.external.argparse import ArgumentParser
9 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
13 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
10 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
14 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
11 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
15 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
12 from IPython.frontend.qt.kernelmanager import QtKernelManager
16 from IPython.frontend.qt.kernelmanager import QtKernelManager
13
17
18 #-----------------------------------------------------------------------------
14 # Constants
19 # Constants
20 #-----------------------------------------------------------------------------
21
15 LOCALHOST = '127.0.0.1'
22 LOCALHOST = '127.0.0.1'
16
23
24 #-----------------------------------------------------------------------------
25 # Classes
26 #-----------------------------------------------------------------------------
27
28 class MainWindow(QtGui.QMainWindow):
29
30 #---------------------------------------------------------------------------
31 # 'object' interface
32 #---------------------------------------------------------------------------
33
34 def __init__(self, frontend):
35 """ Create a MainWindow for the specified FrontendWidget.
36 """
37 super(MainWindow, self).__init__()
38 self._frontend = frontend
39 self._frontend.exit_requested.connect(self.close)
40 self.setCentralWidget(frontend)
41
42 #---------------------------------------------------------------------------
43 # QWidget interface
44 #---------------------------------------------------------------------------
45
46 def closeEvent(self, event):
47 """ Reimplemented to prompt the user and close the kernel cleanly.
48 """
49 reply = QtGui.QMessageBox.question(self, self.window().windowTitle(),
50 'Close console?', QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
51 if reply == QtGui.QMessageBox.Yes:
52 self._frontend.kernel_manager.shutdown_kernel()
53 event.accept()
54 else:
55 event.ignore()
56
57 #-----------------------------------------------------------------------------
58 # Main entry point
59 #-----------------------------------------------------------------------------
17
60
18 def main():
61 def main():
19 """ Entry point for application.
62 """ Entry point for application.
20 """
63 """
21 # Parse command line arguments.
64 # Parse command line arguments.
22 parser = ArgumentParser()
65 parser = ArgumentParser()
23 kgroup = parser.add_argument_group('kernel options')
66 kgroup = parser.add_argument_group('kernel options')
24 kgroup.add_argument('-e', '--existing', action='store_true',
67 kgroup.add_argument('-e', '--existing', action='store_true',
25 help='connect to an existing kernel')
68 help='connect to an existing kernel')
26 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
69 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
27 help='set the kernel\'s IP address [default localhost]')
70 help='set the kernel\'s IP address [default localhost]')
28 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
71 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
29 help='set the XREQ channel port [default random]')
72 help='set the XREQ channel port [default random]')
30 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
73 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
31 help='set the SUB channel port [default random]')
74 help='set the SUB channel port [default random]')
32 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
75 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
33 help='set the REP channel port [default random]')
76 help='set the REP channel port [default random]')
34 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
77 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
35 help='set the heartbeat port [default: random]')
78 help='set the heartbeat port [default: random]')
36
79
37 egroup = kgroup.add_mutually_exclusive_group()
80 egroup = kgroup.add_mutually_exclusive_group()
38 egroup.add_argument('--pure', action='store_true', help = \
81 egroup.add_argument('--pure', action='store_true', help = \
39 'use a pure Python kernel instead of an IPython kernel')
82 'use a pure Python kernel instead of an IPython kernel')
40 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
83 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
41 const='auto', help = \
84 const='auto', help = \
42 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
85 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
43 given, the GUI backend is matplotlib's, otherwise use one of: \
86 given, the GUI backend is matplotlib's, otherwise use one of: \
44 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
87 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
45
88
46 wgroup = parser.add_argument_group('widget options')
89 wgroup = parser.add_argument_group('widget options')
47 wgroup.add_argument('--paging', type=str, default='inside',
90 wgroup.add_argument('--paging', type=str, default='inside',
48 choices = ['inside', 'hsplit', 'vsplit', 'none'],
91 choices = ['inside', 'hsplit', 'vsplit', 'none'],
49 help='set the paging style [default inside]')
92 help='set the paging style [default inside]')
50 wgroup.add_argument('--rich', action='store_true',
93 wgroup.add_argument('--rich', action='store_true',
51 help='enable rich text support')
94 help='enable rich text support')
52 wgroup.add_argument('--gui-completion', action='store_true',
95 wgroup.add_argument('--gui-completion', action='store_true',
53 help='use a GUI widget for tab completion')
96 help='use a GUI widget for tab completion')
54
97
55 args = parser.parse_args()
98 args = parser.parse_args()
56
99
57 # Don't let Qt or ZMQ swallow KeyboardInterupts.
100 # Don't let Qt or ZMQ swallow KeyboardInterupts.
58 import signal
101 import signal
59 signal.signal(signal.SIGINT, signal.SIG_DFL)
102 signal.signal(signal.SIGINT, signal.SIG_DFL)
60
103
61 # Create a KernelManager and start a kernel.
104 # Create a KernelManager and start a kernel.
62 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
105 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
63 sub_address=(args.ip, args.sub),
106 sub_address=(args.ip, args.sub),
64 rep_address=(args.ip, args.rep),
107 rep_address=(args.ip, args.rep),
65 hb_address=(args.ip, args.hb))
108 hb_address=(args.ip, args.hb))
66 if args.ip == LOCALHOST and not args.existing:
109 if args.ip == LOCALHOST and not args.existing:
67 if args.pure:
110 if args.pure:
68 kernel_manager.start_kernel(ipython=False)
111 kernel_manager.start_kernel(ipython=False)
69 elif args.pylab:
112 elif args.pylab:
70 if args.rich:
113 if args.rich:
71 kernel_manager.start_kernel(pylab='payload-svg')
114 kernel_manager.start_kernel(pylab='payload-svg')
72 else:
115 else:
73 if args.pylab == 'auto':
116 if args.pylab == 'auto':
74 kernel_manager.start_kernel(pylab='qt4')
117 kernel_manager.start_kernel(pylab='qt4')
75 else:
118 else:
76 kernel_manager.start_kernel(pylab=args.pylab)
119 kernel_manager.start_kernel(pylab=args.pylab)
77 else:
120 else:
78 kernel_manager.start_kernel()
121 kernel_manager.start_kernel()
79 kernel_manager.start_channels()
122 kernel_manager.start_channels()
80
123
81 # Create the widget.
124 # Create the widget.
82 app = QtGui.QApplication([])
125 app = QtGui.QApplication([])
83 if args.pure:
126 if args.pure:
84 kind = 'rich' if args.rich else 'plain'
127 kind = 'rich' if args.rich else 'plain'
85 widget = FrontendWidget(kind=kind, paging=args.paging)
128 widget = FrontendWidget(kind=kind, paging=args.paging)
86 elif args.rich:
129 elif args.rich:
87 widget = RichIPythonWidget(paging=args.paging)
130 widget = RichIPythonWidget(paging=args.paging)
88 else:
131 else:
89 widget = IPythonWidget(paging=args.paging)
132 widget = IPythonWidget(paging=args.paging)
90 widget.gui_completion = args.gui_completion
133 widget.gui_completion = args.gui_completion
91 widget.kernel_manager = kernel_manager
134 widget.kernel_manager = kernel_manager
92 widget.setWindowTitle('Python' if args.pure else 'IPython')
135
93 widget.show()
136 # Create the main window.
137 window = MainWindow(widget)
138 window.setWindowTitle('Python' if args.pure else 'IPython')
139 window.show()
94
140
95 # Start the application main loop.
141 # Start the application main loop.
96 app.exec_()
142 app.exec_()
97
143
98
144
99 if __name__ == '__main__':
145 if __name__ == '__main__':
100 main()
146 main()
@@ -1,785 +1,806 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 import io
32 from IPython.utils import io
33 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
33 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
34 from session import Session
34 from session import Session
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Constants and exceptions
37 # Constants and exceptions
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39
39
40 LOCALHOST = '127.0.0.1'
40 LOCALHOST = '127.0.0.1'
41
41
42 class InvalidPortNumber(Exception):
42 class InvalidPortNumber(Exception):
43 pass
43 pass
44
44
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46 # Utility functions
46 # Utility functions
47 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
48
48
49 # some utilities to validate message structure, these might get moved elsewhere
49 # some utilities to validate message structure, these might get moved elsewhere
50 # if they prove to have more generic utility
50 # if they prove to have more generic utility
51
51
52 def validate_string_list(lst):
52 def validate_string_list(lst):
53 """Validate that the input is a list of strings.
53 """Validate that the input is a list of strings.
54
54
55 Raises ValueError if not."""
55 Raises ValueError if not."""
56 if not isinstance(lst, list):
56 if not isinstance(lst, list):
57 raise ValueError('input %r must be a list' % lst)
57 raise ValueError('input %r must be a list' % lst)
58 for x in lst:
58 for x in lst:
59 if not isinstance(x, basestring):
59 if not isinstance(x, basestring):
60 raise ValueError('element %r in list must be a string' % x)
60 raise ValueError('element %r in list must be a string' % x)
61
61
62
62
63 def validate_string_dict(dct):
63 def validate_string_dict(dct):
64 """Validate that the input is a dict with string keys and values.
64 """Validate that the input is a dict with string keys and values.
65
65
66 Raises ValueError if not."""
66 Raises ValueError if not."""
67 for k,v in dct.iteritems():
67 for k,v in dct.iteritems():
68 if not isinstance(k, basestring):
68 if not isinstance(k, basestring):
69 raise ValueError('key %r in dict must be a string' % k)
69 raise ValueError('key %r in dict must be a string' % k)
70 if not isinstance(v, basestring):
70 if not isinstance(v, basestring):
71 raise ValueError('value %r in dict must be a string' % v)
71 raise ValueError('value %r in dict must be a string' % v)
72
72
73
73
74 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
75 # ZMQ Socket Channel classes
75 # ZMQ Socket Channel classes
76 #-----------------------------------------------------------------------------
76 #-----------------------------------------------------------------------------
77
77
78 class ZmqSocketChannel(Thread):
78 class ZmqSocketChannel(Thread):
79 """The base class for the channels that use ZMQ sockets.
79 """The base class for the channels that use ZMQ sockets.
80 """
80 """
81 context = None
81 context = None
82 session = None
82 session = None
83 socket = None
83 socket = None
84 ioloop = None
84 ioloop = None
85 iostate = None
85 iostate = None
86 _address = None
86 _address = None
87
87
88 def __init__(self, context, session, address):
88 def __init__(self, context, session, address):
89 """Create a channel
89 """Create a channel
90
90
91 Parameters
91 Parameters
92 ----------
92 ----------
93 context : :class:`zmq.Context`
93 context : :class:`zmq.Context`
94 The ZMQ context to use.
94 The ZMQ context to use.
95 session : :class:`session.Session`
95 session : :class:`session.Session`
96 The session to use.
96 The session to use.
97 address : tuple
97 address : tuple
98 Standard (ip, port) tuple that the kernel is listening on.
98 Standard (ip, port) tuple that the kernel is listening on.
99 """
99 """
100 super(ZmqSocketChannel, self).__init__()
100 super(ZmqSocketChannel, self).__init__()
101 self.daemon = True
101 self.daemon = True
102
102
103 self.context = context
103 self.context = context
104 self.session = session
104 self.session = session
105 if address[1] == 0:
105 if address[1] == 0:
106 message = 'The port number for a channel cannot be 0.'
106 message = 'The port number for a channel cannot be 0.'
107 raise InvalidPortNumber(message)
107 raise InvalidPortNumber(message)
108 self._address = address
108 self._address = address
109
109
110 def stop(self):
110 def stop(self):
111 """Stop the channel's activity.
111 """Stop the channel's activity.
112
112
113 This calls :method:`Thread.join` and returns when the thread
113 This calls :method:`Thread.join` and returns when the thread
114 terminates. :class:`RuntimeError` will be raised if
114 terminates. :class:`RuntimeError` will be raised if
115 :method:`self.start` is called again.
115 :method:`self.start` is called again.
116 """
116 """
117 self.join()
117 self.join()
118
118
119 @property
119 @property
120 def address(self):
120 def address(self):
121 """Get the channel's address as an (ip, port) tuple.
121 """Get the channel's address as an (ip, port) tuple.
122
122
123 By the default, the address is (localhost, 0), where 0 means a random
123 By the default, the address is (localhost, 0), where 0 means a random
124 port.
124 port.
125 """
125 """
126 return self._address
126 return self._address
127
127
128 def add_io_state(self, state):
128 def add_io_state(self, state):
129 """Add IO state to the eventloop.
129 """Add IO state to the eventloop.
130
130
131 Parameters
131 Parameters
132 ----------
132 ----------
133 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
133 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
134 The IO state flag to set.
134 The IO state flag to set.
135
135
136 This is thread safe as it uses the thread safe IOLoop.add_callback.
136 This is thread safe as it uses the thread safe IOLoop.add_callback.
137 """
137 """
138 def add_io_state_callback():
138 def add_io_state_callback():
139 if not self.iostate & state:
139 if not self.iostate & state:
140 self.iostate = self.iostate | state
140 self.iostate = self.iostate | state
141 self.ioloop.update_handler(self.socket, self.iostate)
141 self.ioloop.update_handler(self.socket, self.iostate)
142 self.ioloop.add_callback(add_io_state_callback)
142 self.ioloop.add_callback(add_io_state_callback)
143
143
144 def drop_io_state(self, state):
144 def drop_io_state(self, state):
145 """Drop IO state from the eventloop.
145 """Drop IO state from the eventloop.
146
146
147 Parameters
147 Parameters
148 ----------
148 ----------
149 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
149 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
150 The IO state flag to set.
150 The IO state flag to set.
151
151
152 This is thread safe as it uses the thread safe IOLoop.add_callback.
152 This is thread safe as it uses the thread safe IOLoop.add_callback.
153 """
153 """
154 def drop_io_state_callback():
154 def drop_io_state_callback():
155 if self.iostate & state:
155 if self.iostate & state:
156 self.iostate = self.iostate & (~state)
156 self.iostate = self.iostate & (~state)
157 self.ioloop.update_handler(self.socket, self.iostate)
157 self.ioloop.update_handler(self.socket, self.iostate)
158 self.ioloop.add_callback(drop_io_state_callback)
158 self.ioloop.add_callback(drop_io_state_callback)
159
159
160
160
161 class XReqSocketChannel(ZmqSocketChannel):
161 class XReqSocketChannel(ZmqSocketChannel):
162 """The XREQ channel for issues request/replies to the kernel.
162 """The XREQ channel for issues request/replies to the kernel.
163 """
163 """
164
164
165 command_queue = None
165 command_queue = None
166
166
167 def __init__(self, context, session, address):
167 def __init__(self, context, session, address):
168 self.command_queue = Queue()
168 self.command_queue = Queue()
169 super(XReqSocketChannel, self).__init__(context, session, address)
169 super(XReqSocketChannel, self).__init__(context, session, address)
170
170
171 def run(self):
171 def run(self):
172 """The thread's main activity. Call start() instead."""
172 """The thread's main activity. Call start() instead."""
173 self.socket = self.context.socket(zmq.XREQ)
173 self.socket = self.context.socket(zmq.XREQ)
174 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
174 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
175 self.socket.connect('tcp://%s:%i' % self.address)
175 self.socket.connect('tcp://%s:%i' % self.address)
176 self.ioloop = ioloop.IOLoop()
176 self.ioloop = ioloop.IOLoop()
177 self.iostate = POLLERR|POLLIN
177 self.iostate = POLLERR|POLLIN
178 self.ioloop.add_handler(self.socket, self._handle_events,
178 self.ioloop.add_handler(self.socket, self._handle_events,
179 self.iostate)
179 self.iostate)
180 self.ioloop.start()
180 self.ioloop.start()
181
181
182 def stop(self):
182 def stop(self):
183 self.ioloop.stop()
183 self.ioloop.stop()
184 super(XReqSocketChannel, self).stop()
184 super(XReqSocketChannel, self).stop()
185
185
186 def call_handlers(self, msg):
186 def call_handlers(self, msg):
187 """This method is called in the ioloop thread when a message arrives.
187 """This method is called in the ioloop thread when a message arrives.
188
188
189 Subclasses should override this method to handle incoming messages.
189 Subclasses should override this method to handle incoming messages.
190 It is important to remember that this method is called in the thread
190 It is important to remember that this method is called in the thread
191 so that some logic must be done to ensure that the application leve
191 so that some logic must be done to ensure that the application leve
192 handlers are called in the application thread.
192 handlers are called in the application thread.
193 """
193 """
194 raise NotImplementedError('call_handlers must be defined in a subclass.')
194 raise NotImplementedError('call_handlers must be defined in a subclass.')
195
195
196 def execute(self, code, silent=False,
196 def execute(self, code, silent=False,
197 user_variables=None, user_expressions=None):
197 user_variables=None, user_expressions=None):
198 """Execute code in the kernel.
198 """Execute code in the kernel.
199
199
200 Parameters
200 Parameters
201 ----------
201 ----------
202 code : str
202 code : str
203 A string of Python code.
203 A string of Python code.
204
204
205 silent : bool, optional (default False)
205 silent : bool, optional (default False)
206 If set, the kernel will execute the code as quietly possible.
206 If set, the kernel will execute the code as quietly possible.
207
207
208 user_variables : list, optional
208 user_variables : list, optional
209 A list of variable names to pull from the user's namespace. They
209 A list of variable names to pull from the user's namespace. They
210 will come back as a dict with these names as keys and their
210 will come back as a dict with these names as keys and their
211 :func:`repr` as values.
211 :func:`repr` as values.
212
212
213 user_expressions : dict, optional
213 user_expressions : dict, optional
214 A dict with string keys and to pull from the user's
214 A dict with string keys and to pull from the user's
215 namespace. They will come back as a dict with these names as keys
215 namespace. They will come back as a dict with these names as keys
216 and their :func:`repr` as values.
216 and their :func:`repr` as values.
217
217
218 Returns
218 Returns
219 -------
219 -------
220 The msg_id of the message sent.
220 The msg_id of the message sent.
221 """
221 """
222 if user_variables is None:
222 if user_variables is None:
223 user_variables = []
223 user_variables = []
224 if user_expressions is None:
224 if user_expressions is None:
225 user_expressions = {}
225 user_expressions = {}
226
226
227 # Don't waste network traffic if inputs are invalid
227 # Don't waste network traffic if inputs are invalid
228 if not isinstance(code, basestring):
228 if not isinstance(code, basestring):
229 raise ValueError('code %r must be a string' % code)
229 raise ValueError('code %r must be a string' % code)
230 validate_string_list(user_variables)
230 validate_string_list(user_variables)
231 validate_string_dict(user_expressions)
231 validate_string_dict(user_expressions)
232
232
233 # Create class for content/msg creation. Related to, but possibly
233 # Create class for content/msg creation. Related to, but possibly
234 # not in Session.
234 # not in Session.
235 content = dict(code=code, silent=silent,
235 content = dict(code=code, silent=silent,
236 user_variables=user_variables,
236 user_variables=user_variables,
237 user_expressions=user_expressions)
237 user_expressions=user_expressions)
238 msg = self.session.msg('execute_request', content)
238 msg = self.session.msg('execute_request', content)
239 self._queue_request(msg)
239 self._queue_request(msg)
240 return msg['header']['msg_id']
240 return msg['header']['msg_id']
241
241
242 def complete(self, text, line, cursor_pos, block=None):
242 def complete(self, text, line, cursor_pos, block=None):
243 """Tab complete text in the kernel's namespace.
243 """Tab complete text in the kernel's namespace.
244
244
245 Parameters
245 Parameters
246 ----------
246 ----------
247 text : str
247 text : str
248 The text to complete.
248 The text to complete.
249 line : str
249 line : str
250 The full line of text that is the surrounding context for the
250 The full line of text that is the surrounding context for the
251 text to complete.
251 text to complete.
252 cursor_pos : int
252 cursor_pos : int
253 The position of the cursor in the line where the completion was
253 The position of the cursor in the line where the completion was
254 requested.
254 requested.
255 block : str, optional
255 block : str, optional
256 The full block of code in which the completion is being requested.
256 The full block of code in which the completion is being requested.
257
257
258 Returns
258 Returns
259 -------
259 -------
260 The msg_id of the message sent.
260 The msg_id of the message sent.
261 """
261 """
262 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
262 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
263 msg = self.session.msg('complete_request', content)
263 msg = self.session.msg('complete_request', content)
264 self._queue_request(msg)
264 self._queue_request(msg)
265 return msg['header']['msg_id']
265 return msg['header']['msg_id']
266
266
267 def object_info(self, oname):
267 def object_info(self, oname):
268 """Get metadata information about an object.
268 """Get metadata information about an object.
269
269
270 Parameters
270 Parameters
271 ----------
271 ----------
272 oname : str
272 oname : str
273 A string specifying the object name.
273 A string specifying the object name.
274
274
275 Returns
275 Returns
276 -------
276 -------
277 The msg_id of the message sent.
277 The msg_id of the message sent.
278 """
278 """
279 content = dict(oname=oname)
279 content = dict(oname=oname)
280 msg = self.session.msg('object_info_request', content)
280 msg = self.session.msg('object_info_request', content)
281 self._queue_request(msg)
281 self._queue_request(msg)
282 return msg['header']['msg_id']
282 return msg['header']['msg_id']
283
283
284 def history(self, index=None, raw=False, output=True):
284 def history(self, index=None, raw=False, output=True):
285 """Get the history list.
285 """Get the history list.
286
286
287 Parameters
287 Parameters
288 ----------
288 ----------
289 index : n or (n1, n2) or None
289 index : n or (n1, n2) or None
290 If n, then the last entries. If a tuple, then all in
290 If n, then the last entries. If a tuple, then all in
291 range(n1, n2). If None, then all entries. Raises IndexError if
291 range(n1, n2). If None, then all entries. Raises IndexError if
292 the format of index is incorrect.
292 the format of index is incorrect.
293 raw : bool
293 raw : bool
294 If True, return the raw input.
294 If True, return the raw input.
295 output : bool
295 output : bool
296 If True, then return the output as well.
296 If True, then return the output as well.
297
297
298 Returns
298 Returns
299 -------
299 -------
300 The msg_id of the message sent.
300 The msg_id of the message sent.
301 """
301 """
302 content = dict(index=index, raw=raw, output=output)
302 content = dict(index=index, raw=raw, output=output)
303 msg = self.session.msg('history_request', content)
303 msg = self.session.msg('history_request', content)
304 self._queue_request(msg)
304 self._queue_request(msg)
305 return msg['header']['msg_id']
305 return msg['header']['msg_id']
306
306
307 def _handle_events(self, socket, events):
307 def _handle_events(self, socket, events):
308 if events & POLLERR:
308 if events & POLLERR:
309 self._handle_err()
309 self._handle_err()
310 if events & POLLOUT:
310 if events & POLLOUT:
311 self._handle_send()
311 self._handle_send()
312 if events & POLLIN:
312 if events & POLLIN:
313 self._handle_recv()
313 self._handle_recv()
314
314
315 def _handle_recv(self):
315 def _handle_recv(self):
316 msg = self.socket.recv_json()
316 msg = self.socket.recv_json()
317 self.call_handlers(msg)
317 self.call_handlers(msg)
318
318
319 def _handle_send(self):
319 def _handle_send(self):
320 try:
320 try:
321 msg = self.command_queue.get(False)
321 msg = self.command_queue.get(False)
322 except Empty:
322 except Empty:
323 pass
323 pass
324 else:
324 else:
325 self.socket.send_json(msg)
325 self.socket.send_json(msg)
326 if self.command_queue.empty():
326 if self.command_queue.empty():
327 self.drop_io_state(POLLOUT)
327 self.drop_io_state(POLLOUT)
328
328
329 def _handle_err(self):
329 def _handle_err(self):
330 # We don't want to let this go silently, so eventually we should log.
330 # We don't want to let this go silently, so eventually we should log.
331 raise zmq.ZMQError()
331 raise zmq.ZMQError()
332
332
333 def _queue_request(self, msg):
333 def _queue_request(self, msg):
334 self.command_queue.put(msg)
334 self.command_queue.put(msg)
335 self.add_io_state(POLLOUT)
335 self.add_io_state(POLLOUT)
336
336
337
337
338 class SubSocketChannel(ZmqSocketChannel):
338 class SubSocketChannel(ZmqSocketChannel):
339 """The SUB channel which listens for messages that the kernel publishes.
339 """The SUB channel which listens for messages that the kernel publishes.
340 """
340 """
341
341
342 def __init__(self, context, session, address):
342 def __init__(self, context, session, address):
343 super(SubSocketChannel, self).__init__(context, session, address)
343 super(SubSocketChannel, self).__init__(context, session, address)
344
344
345 def run(self):
345 def run(self):
346 """The thread's main activity. Call start() instead."""
346 """The thread's main activity. Call start() instead."""
347 self.socket = self.context.socket(zmq.SUB)
347 self.socket = self.context.socket(zmq.SUB)
348 self.socket.setsockopt(zmq.SUBSCRIBE,'')
348 self.socket.setsockopt(zmq.SUBSCRIBE,'')
349 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
349 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
350 self.socket.connect('tcp://%s:%i' % self.address)
350 self.socket.connect('tcp://%s:%i' % self.address)
351 self.ioloop = ioloop.IOLoop()
351 self.ioloop = ioloop.IOLoop()
352 self.iostate = POLLIN|POLLERR
352 self.iostate = POLLIN|POLLERR
353 self.ioloop.add_handler(self.socket, self._handle_events,
353 self.ioloop.add_handler(self.socket, self._handle_events,
354 self.iostate)
354 self.iostate)
355 self.ioloop.start()
355 self.ioloop.start()
356
356
357 def stop(self):
357 def stop(self):
358 self.ioloop.stop()
358 self.ioloop.stop()
359 super(SubSocketChannel, self).stop()
359 super(SubSocketChannel, self).stop()
360
360
361 def call_handlers(self, msg):
361 def call_handlers(self, msg):
362 """This method is called in the ioloop thread when a message arrives.
362 """This method is called in the ioloop thread when a message arrives.
363
363
364 Subclasses should override this method to handle incoming messages.
364 Subclasses should override this method to handle incoming messages.
365 It is important to remember that this method is called in the thread
365 It is important to remember that this method is called in the thread
366 so that some logic must be done to ensure that the application leve
366 so that some logic must be done to ensure that the application leve
367 handlers are called in the application thread.
367 handlers are called in the application thread.
368 """
368 """
369 raise NotImplementedError('call_handlers must be defined in a subclass.')
369 raise NotImplementedError('call_handlers must be defined in a subclass.')
370
370
371 def flush(self, timeout=1.0):
371 def flush(self, timeout=1.0):
372 """Immediately processes all pending messages on the SUB channel.
372 """Immediately processes all pending messages on the SUB channel.
373
373
374 Callers should use this method to ensure that :method:`call_handlers`
374 Callers should use this method to ensure that :method:`call_handlers`
375 has been called for all messages that have been received on the
375 has been called for all messages that have been received on the
376 0MQ SUB socket of this channel.
376 0MQ SUB socket of this channel.
377
377
378 This method is thread safe.
378 This method is thread safe.
379
379
380 Parameters
380 Parameters
381 ----------
381 ----------
382 timeout : float, optional
382 timeout : float, optional
383 The maximum amount of time to spend flushing, in seconds. The
383 The maximum amount of time to spend flushing, in seconds. The
384 default is one second.
384 default is one second.
385 """
385 """
386 # We do the IOLoop callback process twice to ensure that the IOLoop
386 # We do the IOLoop callback process twice to ensure that the IOLoop
387 # gets to perform at least one full poll.
387 # gets to perform at least one full poll.
388 stop_time = time.time() + timeout
388 stop_time = time.time() + timeout
389 for i in xrange(2):
389 for i in xrange(2):
390 self._flushed = False
390 self._flushed = False
391 self.ioloop.add_callback(self._flush)
391 self.ioloop.add_callback(self._flush)
392 while not self._flushed and time.time() < stop_time:
392 while not self._flushed and time.time() < stop_time:
393 time.sleep(0.01)
393 time.sleep(0.01)
394
394
395 def _handle_events(self, socket, events):
395 def _handle_events(self, socket, events):
396 # Turn on and off POLLOUT depending on if we have made a request
396 # Turn on and off POLLOUT depending on if we have made a request
397 if events & POLLERR:
397 if events & POLLERR:
398 self._handle_err()
398 self._handle_err()
399 if events & POLLIN:
399 if events & POLLIN:
400 self._handle_recv()
400 self._handle_recv()
401
401
402 def _handle_err(self):
402 def _handle_err(self):
403 # We don't want to let this go silently, so eventually we should log.
403 # We don't want to let this go silently, so eventually we should log.
404 raise zmq.ZMQError()
404 raise zmq.ZMQError()
405
405
406 def _handle_recv(self):
406 def _handle_recv(self):
407 # Get all of the messages we can
407 # Get all of the messages we can
408 while True:
408 while True:
409 try:
409 try:
410 msg = self.socket.recv_json(zmq.NOBLOCK)
410 msg = self.socket.recv_json(zmq.NOBLOCK)
411 except zmq.ZMQError:
411 except zmq.ZMQError:
412 # Check the errno?
412 # Check the errno?
413 # Will this trigger POLLERR?
413 # Will this trigger POLLERR?
414 break
414 break
415 else:
415 else:
416 self.call_handlers(msg)
416 self.call_handlers(msg)
417
417
418 def _flush(self):
418 def _flush(self):
419 """Callback for :method:`self.flush`."""
419 """Callback for :method:`self.flush`."""
420 self._flushed = True
420 self._flushed = True
421
421
422
422
423 class RepSocketChannel(ZmqSocketChannel):
423 class RepSocketChannel(ZmqSocketChannel):
424 """A reply channel to handle raw_input requests that the kernel makes."""
424 """A reply channel to handle raw_input requests that the kernel makes."""
425
425
426 msg_queue = None
426 msg_queue = None
427
427
428 def __init__(self, context, session, address):
428 def __init__(self, context, session, address):
429 self.msg_queue = Queue()
429 self.msg_queue = Queue()
430 super(RepSocketChannel, self).__init__(context, session, address)
430 super(RepSocketChannel, self).__init__(context, session, address)
431
431
432 def run(self):
432 def run(self):
433 """The thread's main activity. Call start() instead."""
433 """The thread's main activity. Call start() instead."""
434 self.socket = self.context.socket(zmq.XREQ)
434 self.socket = self.context.socket(zmq.XREQ)
435 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
435 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
436 self.socket.connect('tcp://%s:%i' % self.address)
436 self.socket.connect('tcp://%s:%i' % self.address)
437 self.ioloop = ioloop.IOLoop()
437 self.ioloop = ioloop.IOLoop()
438 self.iostate = POLLERR|POLLIN
438 self.iostate = POLLERR|POLLIN
439 self.ioloop.add_handler(self.socket, self._handle_events,
439 self.ioloop.add_handler(self.socket, self._handle_events,
440 self.iostate)
440 self.iostate)
441 self.ioloop.start()
441 self.ioloop.start()
442
442
443 def stop(self):
443 def stop(self):
444 self.ioloop.stop()
444 self.ioloop.stop()
445 super(RepSocketChannel, self).stop()
445 super(RepSocketChannel, self).stop()
446
446
447 def call_handlers(self, msg):
447 def call_handlers(self, msg):
448 """This method is called in the ioloop thread when a message arrives.
448 """This method is called in the ioloop thread when a message arrives.
449
449
450 Subclasses should override this method to handle incoming messages.
450 Subclasses should override this method to handle incoming messages.
451 It is important to remember that this method is called in the thread
451 It is important to remember that this method is called in the thread
452 so that some logic must be done to ensure that the application leve
452 so that some logic must be done to ensure that the application leve
453 handlers are called in the application thread.
453 handlers are called in the application thread.
454 """
454 """
455 raise NotImplementedError('call_handlers must be defined in a subclass.')
455 raise NotImplementedError('call_handlers must be defined in a subclass.')
456
456
457 def input(self, string):
457 def input(self, string):
458 """Send a string of raw input to the kernel."""
458 """Send a string of raw input to the kernel."""
459 content = dict(value=string)
459 content = dict(value=string)
460 msg = self.session.msg('input_reply', content)
460 msg = self.session.msg('input_reply', content)
461 self._queue_reply(msg)
461 self._queue_reply(msg)
462
462
463 def _handle_events(self, socket, events):
463 def _handle_events(self, socket, events):
464 if events & POLLERR:
464 if events & POLLERR:
465 self._handle_err()
465 self._handle_err()
466 if events & POLLOUT:
466 if events & POLLOUT:
467 self._handle_send()
467 self._handle_send()
468 if events & POLLIN:
468 if events & POLLIN:
469 self._handle_recv()
469 self._handle_recv()
470
470
471 def _handle_recv(self):
471 def _handle_recv(self):
472 msg = self.socket.recv_json()
472 msg = self.socket.recv_json()
473 self.call_handlers(msg)
473 self.call_handlers(msg)
474
474
475 def _handle_send(self):
475 def _handle_send(self):
476 try:
476 try:
477 msg = self.msg_queue.get(False)
477 msg = self.msg_queue.get(False)
478 except Empty:
478 except Empty:
479 pass
479 pass
480 else:
480 else:
481 self.socket.send_json(msg)
481 self.socket.send_json(msg)
482 if self.msg_queue.empty():
482 if self.msg_queue.empty():
483 self.drop_io_state(POLLOUT)
483 self.drop_io_state(POLLOUT)
484
484
485 def _handle_err(self):
485 def _handle_err(self):
486 # We don't want to let this go silently, so eventually we should log.
486 # We don't want to let this go silently, so eventually we should log.
487 raise zmq.ZMQError()
487 raise zmq.ZMQError()
488
488
489 def _queue_reply(self, msg):
489 def _queue_reply(self, msg):
490 self.msg_queue.put(msg)
490 self.msg_queue.put(msg)
491 self.add_io_state(POLLOUT)
491 self.add_io_state(POLLOUT)
492
492
493
493
494 class HBSocketChannel(ZmqSocketChannel):
494 class HBSocketChannel(ZmqSocketChannel):
495 """The heartbeat channel which monitors the kernel heartbeat."""
495 """The heartbeat channel which monitors the kernel heartbeat."""
496
496
497 time_to_dead = 3.0
497 time_to_dead = 3.0
498 socket = None
498 socket = None
499 poller = None
499 poller = None
500
500
501 def __init__(self, context, session, address):
501 def __init__(self, context, session, address):
502 super(HBSocketChannel, self).__init__(context, session, address)
502 super(HBSocketChannel, self).__init__(context, session, address)
503 self._running = False
503 self._running = False
504
504
505 def _create_socket(self):
505 def _create_socket(self):
506 self.socket = self.context.socket(zmq.REQ)
506 self.socket = self.context.socket(zmq.REQ)
507 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
507 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
508 self.socket.connect('tcp://%s:%i' % self.address)
508 self.socket.connect('tcp://%s:%i' % self.address)
509 self.poller = zmq.Poller()
509 self.poller = zmq.Poller()
510 self.poller.register(self.socket, zmq.POLLIN)
510 self.poller.register(self.socket, zmq.POLLIN)
511
511
512 def run(self):
512 def run(self):
513 """The thread's main activity. Call start() instead."""
513 """The thread's main activity. Call start() instead."""
514 self._create_socket()
514 self._create_socket()
515 self._running = True
515 self._running = True
516 # Wait 2 seconds for the kernel to come up and the sockets to auto
516 # Wait 2 seconds for the kernel to come up and the sockets to auto
517 # connect. If we don't we will see the kernel as dead. Also, before
517 # connect. If we don't we will see the kernel as dead. Also, before
518 # the sockets are connected, the poller.poll line below is returning
518 # the sockets are connected, the poller.poll line below is returning
519 # too fast. This avoids that because the polling doesn't start until
519 # too fast. This avoids that because the polling doesn't start until
520 # after the sockets are connected.
520 # after the sockets are connected.
521 time.sleep(2.0)
521 time.sleep(2.0)
522 while self._running:
522 while self._running:
523 since_last_heartbeat = 0.0
523 since_last_heartbeat = 0.0
524 request_time = time.time()
524 request_time = time.time()
525 try:
525 try:
526 #io.rprint('Ping from HB channel') # dbg
526 #io.rprint('Ping from HB channel') # dbg
527 self.socket.send_json('ping')
527 self.socket.send_json('ping')
528 except zmq.ZMQError, e:
528 except zmq.ZMQError, e:
529 #io.rprint('*** HB Error:', e) # dbg
529 #io.rprint('*** HB Error:', e) # dbg
530 if e.errno == zmq.EFSM:
530 if e.errno == zmq.EFSM:
531 #io.rprint('sleep...', self.time_to_dead) # dbg
531 #io.rprint('sleep...', self.time_to_dead) # dbg
532 time.sleep(self.time_to_dead)
532 time.sleep(self.time_to_dead)
533 self._create_socket()
533 self._create_socket()
534 else:
534 else:
535 raise
535 raise
536 else:
536 else:
537 while True:
537 while True:
538 try:
538 try:
539 self.socket.recv_json(zmq.NOBLOCK)
539 self.socket.recv_json(zmq.NOBLOCK)
540 except zmq.ZMQError, e:
540 except zmq.ZMQError, e:
541 #io.rprint('*** HB Error 2:', e) # dbg
541 #io.rprint('*** HB Error 2:', e) # dbg
542 if e.errno == zmq.EAGAIN:
542 if e.errno == zmq.EAGAIN:
543 before_poll = time.time()
543 before_poll = time.time()
544 until_dead = self.time_to_dead - (before_poll -
544 until_dead = self.time_to_dead - (before_poll -
545 request_time)
545 request_time)
546
546
547 # When the return value of poll() is an empty list,
547 # When the return value of poll() is an empty list,
548 # that is when things have gone wrong (zeromq bug).
548 # that is when things have gone wrong (zeromq bug).
549 # As long as it is not an empty list, poll is
549 # As long as it is not an empty list, poll is
550 # working correctly even if it returns quickly.
550 # working correctly even if it returns quickly.
551 # Note: poll timeout is in milliseconds.
551 # Note: poll timeout is in milliseconds.
552 self.poller.poll(1000*until_dead)
552 self.poller.poll(1000*until_dead)
553
553
554 since_last_heartbeat = time.time() - request_time
554 since_last_heartbeat = time.time() - request_time
555 if since_last_heartbeat > self.time_to_dead:
555 if since_last_heartbeat > self.time_to_dead:
556 self.call_handlers(since_last_heartbeat)
556 self.call_handlers(since_last_heartbeat)
557 break
557 break
558 else:
558 else:
559 # FIXME: We should probably log this instead.
559 # FIXME: We should probably log this instead.
560 raise
560 raise
561 else:
561 else:
562 until_dead = self.time_to_dead - (time.time() -
562 until_dead = self.time_to_dead - (time.time() -
563 request_time)
563 request_time)
564 if until_dead > 0.0:
564 if until_dead > 0.0:
565 #io.rprint('sleep...', self.time_to_dead) # dbg
565 #io.rprint('sleep...', self.time_to_dead) # dbg
566 time.sleep(until_dead)
566 time.sleep(until_dead)
567 break
567 break
568
568
569 def stop(self):
569 def stop(self):
570 self._running = False
570 self._running = False
571 super(HBSocketChannel, self).stop()
571 super(HBSocketChannel, self).stop()
572
572
573 def call_handlers(self, since_last_heartbeat):
573 def call_handlers(self, since_last_heartbeat):
574 """This method is called in the ioloop thread when a message arrives.
574 """This method is called in the ioloop thread when a message arrives.
575
575
576 Subclasses should override this method to handle incoming messages.
576 Subclasses should override this method to handle incoming messages.
577 It is important to remember that this method is called in the thread
577 It is important to remember that this method is called in the thread
578 so that some logic must be done to ensure that the application leve
578 so that some logic must be done to ensure that the application leve
579 handlers are called in the application thread.
579 handlers are called in the application thread.
580 """
580 """
581 raise NotImplementedError('call_handlers must be defined in a subclass.')
581 raise NotImplementedError('call_handlers must be defined in a subclass.')
582
582
583
583
584 #-----------------------------------------------------------------------------
584 #-----------------------------------------------------------------------------
585 # Main kernel manager class
585 # Main kernel manager class
586 #-----------------------------------------------------------------------------
586 #-----------------------------------------------------------------------------
587
587
588 class KernelManager(HasTraits):
588 class KernelManager(HasTraits):
589 """ Manages a kernel for a frontend.
589 """ Manages a kernel for a frontend.
590
590
591 The SUB channel is for the frontend to receive messages published by the
591 The SUB channel is for the frontend to receive messages published by the
592 kernel.
592 kernel.
593
593
594 The REQ channel is for the frontend to make requests of the kernel.
594 The REQ channel is for the frontend to make requests of the kernel.
595
595
596 The REP channel is for the kernel to request stdin (raw_input) from the
596 The REP channel is for the kernel to request stdin (raw_input) from the
597 frontend.
597 frontend.
598 """
598 """
599 # The PyZMQ Context to use for communication with the kernel.
599 # The PyZMQ Context to use for communication with the kernel.
600 context = Instance(zmq.Context,(),{})
600 context = Instance(zmq.Context,(),{})
601
601
602 # The Session to use for communication with the kernel.
602 # The Session to use for communication with the kernel.
603 session = Instance(Session,(),{})
603 session = Instance(Session,(),{})
604
604
605 # The kernel process with which the KernelManager is communicating.
605 # The kernel process with which the KernelManager is communicating.
606 kernel = Instance(Popen)
606 kernel = Instance(Popen)
607
607
608 # The addresses for the communication channels.
608 # The addresses for the communication channels.
609 xreq_address = TCPAddress((LOCALHOST, 0))
609 xreq_address = TCPAddress((LOCALHOST, 0))
610 sub_address = TCPAddress((LOCALHOST, 0))
610 sub_address = TCPAddress((LOCALHOST, 0))
611 rep_address = TCPAddress((LOCALHOST, 0))
611 rep_address = TCPAddress((LOCALHOST, 0))
612 hb_address = TCPAddress((LOCALHOST, 0))
612 hb_address = TCPAddress((LOCALHOST, 0))
613
613
614 # The classes to use for the various channels.
614 # The classes to use for the various channels.
615 xreq_channel_class = Type(XReqSocketChannel)
615 xreq_channel_class = Type(XReqSocketChannel)
616 sub_channel_class = Type(SubSocketChannel)
616 sub_channel_class = Type(SubSocketChannel)
617 rep_channel_class = Type(RepSocketChannel)
617 rep_channel_class = Type(RepSocketChannel)
618 hb_channel_class = Type(HBSocketChannel)
618 hb_channel_class = Type(HBSocketChannel)
619
619
620 # Protected traits.
620 # Protected traits.
621 _launch_args = Any
621 _launch_args = Any
622 _xreq_channel = Any
622 _xreq_channel = Any
623 _sub_channel = Any
623 _sub_channel = Any
624 _rep_channel = Any
624 _rep_channel = Any
625 _hb_channel = Any
625 _hb_channel = Any
626
626
627 #--------------------------------------------------------------------------
627 #--------------------------------------------------------------------------
628 # Channel management methods:
628 # Channel management methods:
629 #--------------------------------------------------------------------------
629 #--------------------------------------------------------------------------
630
630
631 def start_channels(self):
631 def start_channels(self):
632 """Starts the channels for this kernel.
632 """Starts the channels for this kernel.
633
633
634 This will create the channels if they do not exist and then start
634 This will create the channels if they do not exist and then start
635 them. If port numbers of 0 are being used (random ports) then you
635 them. If port numbers of 0 are being used (random ports) then you
636 must first call :method:`start_kernel`. If the channels have been
636 must first call :method:`start_kernel`. If the channels have been
637 stopped and you call this, :class:`RuntimeError` will be raised.
637 stopped and you call this, :class:`RuntimeError` will be raised.
638 """
638 """
639 self.xreq_channel.start()
639 self.xreq_channel.start()
640 self.sub_channel.start()
640 self.sub_channel.start()
641 self.rep_channel.start()
641 self.rep_channel.start()
642 self.hb_channel.start()
642 self.hb_channel.start()
643
643
644 def stop_channels(self):
644 def stop_channels(self):
645 """Stops the channels for this kernel.
645 """Stops the channels for this kernel.
646
646
647 This stops the channels by joining their threads. If the channels
647 This stops the channels by joining their threads. If the channels
648 were not started, :class:`RuntimeError` will be raised.
648 were not started, :class:`RuntimeError` will be raised.
649 """
649 """
650 self.xreq_channel.stop()
650 self.xreq_channel.stop()
651 self.sub_channel.stop()
651 self.sub_channel.stop()
652 self.rep_channel.stop()
652 self.rep_channel.stop()
653 self.hb_channel.stop()
653 self.hb_channel.stop()
654
654
655 @property
655 @property
656 def channels_running(self):
656 def channels_running(self):
657 """Are all of the channels created and running?"""
657 """Are all of the channels created and running?"""
658 return self.xreq_channel.is_alive() \
658 return self.xreq_channel.is_alive() \
659 and self.sub_channel.is_alive() \
659 and self.sub_channel.is_alive() \
660 and self.rep_channel.is_alive() \
660 and self.rep_channel.is_alive() \
661 and self.hb_channel.is_alive()
661 and self.hb_channel.is_alive()
662
662
663 #--------------------------------------------------------------------------
663 #--------------------------------------------------------------------------
664 # Kernel process management methods:
664 # Kernel process management methods:
665 #--------------------------------------------------------------------------
665 #--------------------------------------------------------------------------
666
666
667 def start_kernel(self, **kw):
667 def start_kernel(self, **kw):
668 """Starts a kernel process and configures the manager to use it.
668 """Starts a kernel process and configures the manager to use it.
669
669
670 If random ports (port=0) are being used, this method must be called
670 If random ports (port=0) are being used, this method must be called
671 before the channels are created.
671 before the channels are created.
672
672
673 Parameters:
673 Parameters:
674 -----------
674 -----------
675 ipython : bool, optional (default True)
675 ipython : bool, optional (default True)
676 Whether to use an IPython kernel instead of a plain Python kernel.
676 Whether to use an IPython kernel instead of a plain Python kernel.
677 """
677 """
678 xreq, sub, rep, hb = self.xreq_address, self.sub_address, \
678 xreq, sub, rep, hb = self.xreq_address, self.sub_address, \
679 self.rep_address, self.hb_address
679 self.rep_address, self.hb_address
680 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or \
680 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or \
681 rep[0] != LOCALHOST or hb[0] != LOCALHOST:
681 rep[0] != LOCALHOST or hb[0] != LOCALHOST:
682 raise RuntimeError("Can only launch a kernel on localhost."
682 raise RuntimeError("Can only launch a kernel on localhost."
683 "Make sure that the '*_address' attributes are "
683 "Make sure that the '*_address' attributes are "
684 "configured properly.")
684 "configured properly.")
685
685
686 self._launch_args = kw.copy()
686 self._launch_args = kw.copy()
687 if kw.pop('ipython', True):
687 if kw.pop('ipython', True):
688 from ipkernel import launch_kernel as launch
688 from ipkernel import launch_kernel
689 else:
689 else:
690 from pykernel import launch_kernel as launch
690 from pykernel import launch_kernel
691 self.kernel, xrep, pub, req, hb = launch(
691 self.kernel, xrep, pub, req, hb = launch_kernel(
692 xrep_port=xreq[1], pub_port=sub[1],
692 xrep_port=xreq[1], pub_port=sub[1],
693 req_port=rep[1], hb_port=hb[1], **kw)
693 req_port=rep[1], hb_port=hb[1], **kw)
694 self.xreq_address = (LOCALHOST, xrep)
694 self.xreq_address = (LOCALHOST, xrep)
695 self.sub_address = (LOCALHOST, pub)
695 self.sub_address = (LOCALHOST, pub)
696 self.rep_address = (LOCALHOST, req)
696 self.rep_address = (LOCALHOST, req)
697 self.hb_address = (LOCALHOST, hb)
697 self.hb_address = (LOCALHOST, hb)
698
698
699 def shutdown_kernel(self):
700 """ Attempts to the stop the kernel process cleanly. If the kernel
701 cannot be stopped, it is killed, if possible.
702 """
703 # Send quit message to kernel. Once we implement kernel-side setattr,
704 # this should probably be done that way, but for now this will do.
705 self.xreq_channel.execute('get_ipython().exit_now=True', silent=True)
706
707 # Don't send any additional kernel kill messages immediately, to give
708 # the kernel a chance to properly execute shutdown actions. Wait for at
709 # most 2s, checking every 0.1s.
710 for i in range(20):
711 if self.is_alive:
712 time.sleep(0.1)
713 else:
714 break
715 else:
716 # OK, we've waited long enough.
717 if self.has_kernel:
718 self.kill_kernel()
719
699 def restart_kernel(self):
720 def restart_kernel(self):
700 """Restarts a kernel with the same arguments that were used to launch
721 """Restarts a kernel with the same arguments that were used to launch
701 it. If the old kernel was launched with random ports, the same ports
722 it. If the old kernel was launched with random ports, the same ports
702 will be used for the new kernel.
723 will be used for the new kernel.
703 """
724 """
704 if self._launch_args is None:
725 if self._launch_args is None:
705 raise RuntimeError("Cannot restart the kernel. "
726 raise RuntimeError("Cannot restart the kernel. "
706 "No previous call to 'start_kernel'.")
727 "No previous call to 'start_kernel'.")
707 else:
728 else:
708 if self.has_kernel:
729 if self.has_kernel:
709 self.kill_kernel()
730 self.kill_kernel()
710 self.start_kernel(**self._launch_args)
731 self.start_kernel(**self._launch_args)
711
732
712 @property
733 @property
713 def has_kernel(self):
734 def has_kernel(self):
714 """Returns whether a kernel process has been specified for the kernel
735 """Returns whether a kernel process has been specified for the kernel
715 manager.
736 manager.
716 """
737 """
717 return self.kernel is not None
738 return self.kernel is not None
718
739
719 def kill_kernel(self):
740 def kill_kernel(self):
720 """ Kill the running kernel. """
741 """ Kill the running kernel. """
721 if self.kernel is not None:
742 if self.kernel is not None:
722 self.kernel.kill()
743 self.kernel.kill()
723 self.kernel = None
744 self.kernel = None
724 else:
745 else:
725 raise RuntimeError("Cannot kill kernel. No kernel is running!")
746 raise RuntimeError("Cannot kill kernel. No kernel is running!")
726
747
727 def signal_kernel(self, signum):
748 def signal_kernel(self, signum):
728 """ Sends a signal to the kernel. """
749 """ Sends a signal to the kernel. """
729 if self.kernel is not None:
750 if self.kernel is not None:
730 self.kernel.send_signal(signum)
751 self.kernel.send_signal(signum)
731 else:
752 else:
732 raise RuntimeError("Cannot signal kernel. No kernel is running!")
753 raise RuntimeError("Cannot signal kernel. No kernel is running!")
733
754
734 @property
755 @property
735 def is_alive(self):
756 def is_alive(self):
736 """Is the kernel process still running?"""
757 """Is the kernel process still running?"""
737 if self.kernel is not None:
758 if self.kernel is not None:
738 if self.kernel.poll() is None:
759 if self.kernel.poll() is None:
739 return True
760 return True
740 else:
761 else:
741 return False
762 return False
742 else:
763 else:
743 # We didn't start the kernel with this KernelManager so we don't
764 # We didn't start the kernel with this KernelManager so we don't
744 # know if it is running. We should use a heartbeat for this case.
765 # know if it is running. We should use a heartbeat for this case.
745 return True
766 return True
746
767
747 #--------------------------------------------------------------------------
768 #--------------------------------------------------------------------------
748 # Channels used for communication with the kernel:
769 # Channels used for communication with the kernel:
749 #--------------------------------------------------------------------------
770 #--------------------------------------------------------------------------
750
771
751 @property
772 @property
752 def xreq_channel(self):
773 def xreq_channel(self):
753 """Get the REQ socket channel object to make requests of the kernel."""
774 """Get the REQ socket channel object to make requests of the kernel."""
754 if self._xreq_channel is None:
775 if self._xreq_channel is None:
755 self._xreq_channel = self.xreq_channel_class(self.context,
776 self._xreq_channel = self.xreq_channel_class(self.context,
756 self.session,
777 self.session,
757 self.xreq_address)
778 self.xreq_address)
758 return self._xreq_channel
779 return self._xreq_channel
759
780
760 @property
781 @property
761 def sub_channel(self):
782 def sub_channel(self):
762 """Get the SUB socket channel object."""
783 """Get the SUB socket channel object."""
763 if self._sub_channel is None:
784 if self._sub_channel is None:
764 self._sub_channel = self.sub_channel_class(self.context,
785 self._sub_channel = self.sub_channel_class(self.context,
765 self.session,
786 self.session,
766 self.sub_address)
787 self.sub_address)
767 return self._sub_channel
788 return self._sub_channel
768
789
769 @property
790 @property
770 def rep_channel(self):
791 def rep_channel(self):
771 """Get the REP socket channel object to handle stdin (raw_input)."""
792 """Get the REP socket channel object to handle stdin (raw_input)."""
772 if self._rep_channel is None:
793 if self._rep_channel is None:
773 self._rep_channel = self.rep_channel_class(self.context,
794 self._rep_channel = self.rep_channel_class(self.context,
774 self.session,
795 self.session,
775 self.rep_address)
796 self.rep_address)
776 return self._rep_channel
797 return self._rep_channel
777
798
778 @property
799 @property
779 def hb_channel(self):
800 def hb_channel(self):
780 """Get the REP socket channel object to handle stdin (raw_input)."""
801 """Get the REP socket channel object to handle stdin (raw_input)."""
781 if self._hb_channel is None:
802 if self._hb_channel is None:
782 self._hb_channel = self.hb_channel_class(self.context,
803 self._hb_channel = self.hb_channel_class(self.context,
783 self.session,
804 self.session,
784 self.hb_address)
805 self.hb_address)
785 return self._hb_channel
806 return self._hb_channel
General Comments 0
You need to be logged in to leave comments. Login now