##// END OF EJS Templates
add Widget._transform_prompt staticmethod...
MinRK -
Show More
@@ -1,737 +1,738 b''
1 from __future__ import print_function
1 from __future__ import print_function
2
2
3 # Standard library imports
3 # Standard library imports
4 from collections import namedtuple
4 from collections import namedtuple
5 import sys
5 import sys
6 import time
6 import time
7 import uuid
7 import uuid
8
8
9 # System library imports
9 # System library imports
10 from pygments.lexers import PythonLexer
10 from pygments.lexers import PythonLexer
11 from IPython.external import qt
11 from IPython.external import qt
12 from IPython.external.qt import QtCore, QtGui
12 from IPython.external.qt import QtCore, QtGui
13
13
14 # Local imports
14 # Local imports
15 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
15 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
16 from IPython.core.oinspect import call_tip
16 from IPython.core.oinspect import call_tip
17 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
17 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
18 from IPython.utils.traitlets import Bool, Instance, Unicode
18 from IPython.utils.traitlets import Bool, Instance, Unicode
19 from bracket_matcher import BracketMatcher
19 from bracket_matcher import BracketMatcher
20 from call_tip_widget import CallTipWidget
20 from call_tip_widget import CallTipWidget
21 from completion_lexer import CompletionLexer
21 from completion_lexer import CompletionLexer
22 from history_console_widget import HistoryConsoleWidget
22 from history_console_widget import HistoryConsoleWidget
23 from pygments_highlighter import PygmentsHighlighter
23 from pygments_highlighter import PygmentsHighlighter
24
24
25
25
26 class FrontendHighlighter(PygmentsHighlighter):
26 class FrontendHighlighter(PygmentsHighlighter):
27 """ A PygmentsHighlighter that understands and ignores prompts.
27 """ A PygmentsHighlighter that understands and ignores prompts.
28 """
28 """
29
29
30 def __init__(self, frontend):
30 def __init__(self, frontend):
31 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 super(FrontendHighlighter, self).__init__(frontend._control.document())
32 self._current_offset = 0
32 self._current_offset = 0
33 self._frontend = frontend
33 self._frontend = frontend
34 self.highlighting_on = False
34 self.highlighting_on = False
35
35
36 def highlightBlock(self, string):
36 def highlightBlock(self, string):
37 """ Highlight a block of text. Reimplemented to highlight selectively.
37 """ Highlight a block of text. Reimplemented to highlight selectively.
38 """
38 """
39 if not self.highlighting_on:
39 if not self.highlighting_on:
40 return
40 return
41
41
42 # The input to this function is a unicode string that may contain
42 # The input to this function is a unicode string that may contain
43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
44 # the string as plain text so we can compare it.
44 # the string as plain text so we can compare it.
45 current_block = self.currentBlock()
45 current_block = self.currentBlock()
46 string = self._frontend._get_block_plain_text(current_block)
46 string = self._frontend._get_block_plain_text(current_block)
47
47
48 # Decide whether to check for the regular or continuation prompt.
48 # Decide whether to check for the regular or continuation prompt.
49 if current_block.contains(self._frontend._prompt_pos):
49 if current_block.contains(self._frontend._prompt_pos):
50 prompt = self._frontend._prompt
50 prompt = self._frontend._prompt
51 else:
51 else:
52 prompt = self._frontend._continuation_prompt
52 prompt = self._frontend._continuation_prompt
53
53
54 # Only highlight if we can identify a prompt, but make sure not to
54 # Only highlight if we can identify a prompt, but make sure not to
55 # highlight the prompt.
55 # highlight the prompt.
56 if string.startswith(prompt):
56 if string.startswith(prompt):
57 self._current_offset = len(prompt)
57 self._current_offset = len(prompt)
58 string = string[len(prompt):]
58 string = string[len(prompt):]
59 super(FrontendHighlighter, self).highlightBlock(string)
59 super(FrontendHighlighter, self).highlightBlock(string)
60
60
61 def rehighlightBlock(self, block):
61 def rehighlightBlock(self, block):
62 """ Reimplemented to temporarily enable highlighting if disabled.
62 """ Reimplemented to temporarily enable highlighting if disabled.
63 """
63 """
64 old = self.highlighting_on
64 old = self.highlighting_on
65 self.highlighting_on = True
65 self.highlighting_on = True
66 super(FrontendHighlighter, self).rehighlightBlock(block)
66 super(FrontendHighlighter, self).rehighlightBlock(block)
67 self.highlighting_on = old
67 self.highlighting_on = old
68
68
69 def setFormat(self, start, count, format):
69 def setFormat(self, start, count, format):
70 """ Reimplemented to highlight selectively.
70 """ Reimplemented to highlight selectively.
71 """
71 """
72 start += self._current_offset
72 start += self._current_offset
73 super(FrontendHighlighter, self).setFormat(start, count, format)
73 super(FrontendHighlighter, self).setFormat(start, count, format)
74
74
75
75
76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 """ A Qt frontend for a generic Python kernel.
77 """ A Qt frontend for a generic Python kernel.
78 """
78 """
79
79
80 # The text to show when the kernel is (re)started.
80 # The text to show when the kernel is (re)started.
81 banner = Unicode()
81 banner = Unicode()
82
82
83 # An option and corresponding signal for overriding the default kernel
83 # An option and corresponding signal for overriding the default kernel
84 # interrupt behavior.
84 # interrupt behavior.
85 custom_interrupt = Bool(False)
85 custom_interrupt = Bool(False)
86 custom_interrupt_requested = QtCore.Signal()
86 custom_interrupt_requested = QtCore.Signal()
87
87
88 # An option and corresponding signals for overriding the default kernel
88 # An option and corresponding signals for overriding the default kernel
89 # restart behavior.
89 # restart behavior.
90 custom_restart = Bool(False)
90 custom_restart = Bool(False)
91 custom_restart_kernel_died = QtCore.Signal(float)
91 custom_restart_kernel_died = QtCore.Signal(float)
92 custom_restart_requested = QtCore.Signal()
92 custom_restart_requested = QtCore.Signal()
93
93
94 # Whether to automatically show calltips on open-parentheses.
94 # Whether to automatically show calltips on open-parentheses.
95 enable_calltips = Bool(True, config=True,
95 enable_calltips = Bool(True, config=True,
96 help="Whether to draw information calltips on open-parentheses.")
96 help="Whether to draw information calltips on open-parentheses.")
97
97
98 # Emitted when a user visible 'execute_request' has been submitted to the
98 # Emitted when a user visible 'execute_request' has been submitted to the
99 # kernel from the FrontendWidget. Contains the code to be executed.
99 # kernel from the FrontendWidget. Contains the code to be executed.
100 executing = QtCore.Signal(object)
100 executing = QtCore.Signal(object)
101
101
102 # Emitted when a user-visible 'execute_reply' has been received from the
102 # Emitted when a user-visible 'execute_reply' has been received from the
103 # kernel and processed by the FrontendWidget. Contains the response message.
103 # kernel and processed by the FrontendWidget. Contains the response message.
104 executed = QtCore.Signal(object)
104 executed = QtCore.Signal(object)
105
105
106 # Emitted when an exit request has been received from the kernel.
106 # Emitted when an exit request has been received from the kernel.
107 exit_requested = QtCore.Signal(object)
107 exit_requested = QtCore.Signal(object)
108
108
109 # Protected class variables.
109 # Protected class variables.
110 _transform_prompt = staticmethod(transform_classic_prompt)
110 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
111 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
111 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
112 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
112 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
113 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
113 _input_splitter_class = InputSplitter
114 _input_splitter_class = InputSplitter
114 _local_kernel = False
115 _local_kernel = False
115 _highlighter = Instance(FrontendHighlighter)
116 _highlighter = Instance(FrontendHighlighter)
116
117
117 #---------------------------------------------------------------------------
118 #---------------------------------------------------------------------------
118 # 'object' interface
119 # 'object' interface
119 #---------------------------------------------------------------------------
120 #---------------------------------------------------------------------------
120
121
121 def __init__(self, *args, **kw):
122 def __init__(self, *args, **kw):
122 super(FrontendWidget, self).__init__(*args, **kw)
123 super(FrontendWidget, self).__init__(*args, **kw)
123 # FIXME: remove this when PySide min version is updated past 1.0.7
124 # FIXME: remove this when PySide min version is updated past 1.0.7
124 # forcefully disable calltips if PySide is < 1.0.7, because they crash
125 # forcefully disable calltips if PySide is < 1.0.7, because they crash
125 if qt.QT_API == qt.QT_API_PYSIDE:
126 if qt.QT_API == qt.QT_API_PYSIDE:
126 import PySide
127 import PySide
127 if PySide.__version_info__ < (1,0,7):
128 if PySide.__version_info__ < (1,0,7):
128 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
129 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
129 self.enable_calltips = False
130 self.enable_calltips = False
130
131
131 # FrontendWidget protected variables.
132 # FrontendWidget protected variables.
132 self._bracket_matcher = BracketMatcher(self._control)
133 self._bracket_matcher = BracketMatcher(self._control)
133 self._call_tip_widget = CallTipWidget(self._control)
134 self._call_tip_widget = CallTipWidget(self._control)
134 self._completion_lexer = CompletionLexer(PythonLexer())
135 self._completion_lexer = CompletionLexer(PythonLexer())
135 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
136 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
136 self._hidden = False
137 self._hidden = False
137 self._highlighter = FrontendHighlighter(self)
138 self._highlighter = FrontendHighlighter(self)
138 self._input_splitter = self._input_splitter_class(input_mode='cell')
139 self._input_splitter = self._input_splitter_class(input_mode='cell')
139 self._kernel_manager = None
140 self._kernel_manager = None
140 self._request_info = {}
141 self._request_info = {}
141 self._request_info['execute'] = {};
142 self._request_info['execute'] = {};
142 self._callback_dict = {}
143 self._callback_dict = {}
143
144
144 # Configure the ConsoleWidget.
145 # Configure the ConsoleWidget.
145 self.tab_width = 4
146 self.tab_width = 4
146 self._set_continuation_prompt('... ')
147 self._set_continuation_prompt('... ')
147
148
148 # Configure the CallTipWidget.
149 # Configure the CallTipWidget.
149 self._call_tip_widget.setFont(self.font)
150 self._call_tip_widget.setFont(self.font)
150 self.font_changed.connect(self._call_tip_widget.setFont)
151 self.font_changed.connect(self._call_tip_widget.setFont)
151
152
152 # Configure actions.
153 # Configure actions.
153 action = self._copy_raw_action
154 action = self._copy_raw_action
154 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
155 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
155 action.setEnabled(False)
156 action.setEnabled(False)
156 action.setShortcut(QtGui.QKeySequence(key))
157 action.setShortcut(QtGui.QKeySequence(key))
157 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
158 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
158 action.triggered.connect(self.copy_raw)
159 action.triggered.connect(self.copy_raw)
159 self.copy_available.connect(action.setEnabled)
160 self.copy_available.connect(action.setEnabled)
160 self.addAction(action)
161 self.addAction(action)
161
162
162 # Connect signal handlers.
163 # Connect signal handlers.
163 document = self._control.document()
164 document = self._control.document()
164 document.contentsChange.connect(self._document_contents_change)
165 document.contentsChange.connect(self._document_contents_change)
165
166
166 # Set flag for whether we are connected via localhost.
167 # Set flag for whether we are connected via localhost.
167 self._local_kernel = kw.get('local_kernel',
168 self._local_kernel = kw.get('local_kernel',
168 FrontendWidget._local_kernel)
169 FrontendWidget._local_kernel)
169
170
170 #---------------------------------------------------------------------------
171 #---------------------------------------------------------------------------
171 # 'ConsoleWidget' public interface
172 # 'ConsoleWidget' public interface
172 #---------------------------------------------------------------------------
173 #---------------------------------------------------------------------------
173
174
174 def copy(self):
175 def copy(self):
175 """ Copy the currently selected text to the clipboard, removing prompts.
176 """ Copy the currently selected text to the clipboard, removing prompts.
176 """
177 """
177 if self.layout().currentWidget() == self._page_control :
178 if self._page_control.hasFocus():
178 self._page_control.copy()
179 self._page_control.copy()
179 elif self.layout().currentWidget() == self._control :
180 elif self._control.hasFocus():
180 text = self._control.textCursor().selection().toPlainText()
181 text = self._control.textCursor().selection().toPlainText()
181 if text:
182 if text:
182 lines = map(transform_classic_prompt, text.splitlines())
183 lines = map(self._transform_prompt, text.splitlines())
183 text = '\n'.join(lines)
184 text = '\n'.join(lines)
184 QtGui.QApplication.clipboard().setText(text)
185 QtGui.QApplication.clipboard().setText(text)
185 else:
186 else:
186 self.log.debug("frontend widget : unknown copy target")
187 self.log.debug("frontend widget : unknown copy target")
187
188
188 #---------------------------------------------------------------------------
189 #---------------------------------------------------------------------------
189 # 'ConsoleWidget' abstract interface
190 # 'ConsoleWidget' abstract interface
190 #---------------------------------------------------------------------------
191 #---------------------------------------------------------------------------
191
192
192 def _is_complete(self, source, interactive):
193 def _is_complete(self, source, interactive):
193 """ Returns whether 'source' can be completely processed and a new
194 """ Returns whether 'source' can be completely processed and a new
194 prompt created. When triggered by an Enter/Return key press,
195 prompt created. When triggered by an Enter/Return key press,
195 'interactive' is True; otherwise, it is False.
196 'interactive' is True; otherwise, it is False.
196 """
197 """
197 complete = self._input_splitter.push(source)
198 complete = self._input_splitter.push(source)
198 if interactive:
199 if interactive:
199 complete = not self._input_splitter.push_accepts_more()
200 complete = not self._input_splitter.push_accepts_more()
200 return complete
201 return complete
201
202
202 def _execute(self, source, hidden):
203 def _execute(self, source, hidden):
203 """ Execute 'source'. If 'hidden', do not show any output.
204 """ Execute 'source'. If 'hidden', do not show any output.
204
205
205 See parent class :meth:`execute` docstring for full details.
206 See parent class :meth:`execute` docstring for full details.
206 """
207 """
207 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
208 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
208 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
209 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
209 self._hidden = hidden
210 self._hidden = hidden
210 if not hidden:
211 if not hidden:
211 self.executing.emit(source)
212 self.executing.emit(source)
212
213
213 def _prompt_started_hook(self):
214 def _prompt_started_hook(self):
214 """ Called immediately after a new prompt is displayed.
215 """ Called immediately after a new prompt is displayed.
215 """
216 """
216 if not self._reading:
217 if not self._reading:
217 self._highlighter.highlighting_on = True
218 self._highlighter.highlighting_on = True
218
219
219 def _prompt_finished_hook(self):
220 def _prompt_finished_hook(self):
220 """ Called immediately after a prompt is finished, i.e. when some input
221 """ Called immediately after a prompt is finished, i.e. when some input
221 will be processed and a new prompt displayed.
222 will be processed and a new prompt displayed.
222 """
223 """
223 # Flush all state from the input splitter so the next round of
224 # Flush all state from the input splitter so the next round of
224 # reading input starts with a clean buffer.
225 # reading input starts with a clean buffer.
225 self._input_splitter.reset()
226 self._input_splitter.reset()
226
227
227 if not self._reading:
228 if not self._reading:
228 self._highlighter.highlighting_on = False
229 self._highlighter.highlighting_on = False
229
230
230 def _tab_pressed(self):
231 def _tab_pressed(self):
231 """ Called when the tab key is pressed. Returns whether to continue
232 """ Called when the tab key is pressed. Returns whether to continue
232 processing the event.
233 processing the event.
233 """
234 """
234 # Perform tab completion if:
235 # Perform tab completion if:
235 # 1) The cursor is in the input buffer.
236 # 1) The cursor is in the input buffer.
236 # 2) There is a non-whitespace character before the cursor.
237 # 2) There is a non-whitespace character before the cursor.
237 text = self._get_input_buffer_cursor_line()
238 text = self._get_input_buffer_cursor_line()
238 if text is None:
239 if text is None:
239 return False
240 return False
240 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
241 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
241 if complete:
242 if complete:
242 self._complete()
243 self._complete()
243 return not complete
244 return not complete
244
245
245 #---------------------------------------------------------------------------
246 #---------------------------------------------------------------------------
246 # 'ConsoleWidget' protected interface
247 # 'ConsoleWidget' protected interface
247 #---------------------------------------------------------------------------
248 #---------------------------------------------------------------------------
248
249
249 def _context_menu_make(self, pos):
250 def _context_menu_make(self, pos):
250 """ Reimplemented to add an action for raw copy.
251 """ Reimplemented to add an action for raw copy.
251 """
252 """
252 menu = super(FrontendWidget, self)._context_menu_make(pos)
253 menu = super(FrontendWidget, self)._context_menu_make(pos)
253 for before_action in menu.actions():
254 for before_action in menu.actions():
254 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
255 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
255 QtGui.QKeySequence.ExactMatch:
256 QtGui.QKeySequence.ExactMatch:
256 menu.insertAction(before_action, self._copy_raw_action)
257 menu.insertAction(before_action, self._copy_raw_action)
257 break
258 break
258 return menu
259 return menu
259
260
260 def request_interrupt_kernel(self):
261 def request_interrupt_kernel(self):
261 if self._executing:
262 if self._executing:
262 self.interrupt_kernel()
263 self.interrupt_kernel()
263
264
264 def request_restart_kernel(self):
265 def request_restart_kernel(self):
265 message = 'Are you sure you want to restart the kernel?'
266 message = 'Are you sure you want to restart the kernel?'
266 self.restart_kernel(message, now=False)
267 self.restart_kernel(message, now=False)
267
268
268 def _event_filter_console_keypress(self, event):
269 def _event_filter_console_keypress(self, event):
269 """ Reimplemented for execution interruption and smart backspace.
270 """ Reimplemented for execution interruption and smart backspace.
270 """
271 """
271 key = event.key()
272 key = event.key()
272 if self._control_key_down(event.modifiers(), include_command=False):
273 if self._control_key_down(event.modifiers(), include_command=False):
273
274
274 if key == QtCore.Qt.Key_C and self._executing:
275 if key == QtCore.Qt.Key_C and self._executing:
275 self.request_interrupt_kernel()
276 self.request_interrupt_kernel()
276 return True
277 return True
277
278
278 elif key == QtCore.Qt.Key_Period:
279 elif key == QtCore.Qt.Key_Period:
279 self.request_restart_kernel()
280 self.request_restart_kernel()
280 return True
281 return True
281
282
282 elif not event.modifiers() & QtCore.Qt.AltModifier:
283 elif not event.modifiers() & QtCore.Qt.AltModifier:
283
284
284 # Smart backspace: remove four characters in one backspace if:
285 # Smart backspace: remove four characters in one backspace if:
285 # 1) everything left of the cursor is whitespace
286 # 1) everything left of the cursor is whitespace
286 # 2) the four characters immediately left of the cursor are spaces
287 # 2) the four characters immediately left of the cursor are spaces
287 if key == QtCore.Qt.Key_Backspace:
288 if key == QtCore.Qt.Key_Backspace:
288 col = self._get_input_buffer_cursor_column()
289 col = self._get_input_buffer_cursor_column()
289 cursor = self._control.textCursor()
290 cursor = self._control.textCursor()
290 if col > 3 and not cursor.hasSelection():
291 if col > 3 and not cursor.hasSelection():
291 text = self._get_input_buffer_cursor_line()[:col]
292 text = self._get_input_buffer_cursor_line()[:col]
292 if text.endswith(' ') and not text.strip():
293 if text.endswith(' ') and not text.strip():
293 cursor.movePosition(QtGui.QTextCursor.Left,
294 cursor.movePosition(QtGui.QTextCursor.Left,
294 QtGui.QTextCursor.KeepAnchor, 4)
295 QtGui.QTextCursor.KeepAnchor, 4)
295 cursor.removeSelectedText()
296 cursor.removeSelectedText()
296 return True
297 return True
297
298
298 return super(FrontendWidget, self)._event_filter_console_keypress(event)
299 return super(FrontendWidget, self)._event_filter_console_keypress(event)
299
300
300 def _insert_continuation_prompt(self, cursor):
301 def _insert_continuation_prompt(self, cursor):
301 """ Reimplemented for auto-indentation.
302 """ Reimplemented for auto-indentation.
302 """
303 """
303 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
304 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
304 cursor.insertText(' ' * self._input_splitter.indent_spaces)
305 cursor.insertText(' ' * self._input_splitter.indent_spaces)
305
306
306 #---------------------------------------------------------------------------
307 #---------------------------------------------------------------------------
307 # 'BaseFrontendMixin' abstract interface
308 # 'BaseFrontendMixin' abstract interface
308 #---------------------------------------------------------------------------
309 #---------------------------------------------------------------------------
309
310
310 def _handle_complete_reply(self, rep):
311 def _handle_complete_reply(self, rep):
311 """ Handle replies for tab completion.
312 """ Handle replies for tab completion.
312 """
313 """
313 self.log.debug("complete: %s", rep.get('content', ''))
314 self.log.debug("complete: %s", rep.get('content', ''))
314 cursor = self._get_cursor()
315 cursor = self._get_cursor()
315 info = self._request_info.get('complete')
316 info = self._request_info.get('complete')
316 if info and info.id == rep['parent_header']['msg_id'] and \
317 if info and info.id == rep['parent_header']['msg_id'] and \
317 info.pos == cursor.position():
318 info.pos == cursor.position():
318 text = '.'.join(self._get_context())
319 text = '.'.join(self._get_context())
319 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
320 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
320 self._complete_with_items(cursor, rep['content']['matches'])
321 self._complete_with_items(cursor, rep['content']['matches'])
321
322
322 def _silent_exec_callback(self, expr, callback):
323 def _silent_exec_callback(self, expr, callback):
323 """Silently execute `expr` in the kernel and call `callback` with reply
324 """Silently execute `expr` in the kernel and call `callback` with reply
324
325
325 the `expr` is evaluated silently in the kernel (without) output in
326 the `expr` is evaluated silently in the kernel (without) output in
326 the frontend. Call `callback` with the
327 the frontend. Call `callback` with the
327 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
328 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
328
329
329 Parameters
330 Parameters
330 ----------
331 ----------
331 expr : string
332 expr : string
332 valid string to be executed by the kernel.
333 valid string to be executed by the kernel.
333 callback : function
334 callback : function
334 function accepting one arguement, as a string. The string will be
335 function accepting one arguement, as a string. The string will be
335 the `repr` of the result of evaluating `expr`
336 the `repr` of the result of evaluating `expr`
336
337
337 The `callback` is called with the 'repr()' of the result of `expr` as
338 The `callback` is called with the 'repr()' of the result of `expr` as
338 first argument. To get the object, do 'eval()' onthe passed value.
339 first argument. To get the object, do 'eval()' onthe passed value.
339
340
340 See Also
341 See Also
341 --------
342 --------
342 _handle_exec_callback : private method, deal with calling callback with reply
343 _handle_exec_callback : private method, deal with calling callback with reply
343
344
344 """
345 """
345
346
346 # generate uuid, which would be used as a indication of wether or not
347 # generate uuid, which would be used as a indication of wether or not
347 # the unique request originate from here (can use msg id ?)
348 # the unique request originate from here (can use msg id ?)
348 local_uuid = str(uuid.uuid1())
349 local_uuid = str(uuid.uuid1())
349 msg_id = self.kernel_manager.shell_channel.execute('',
350 msg_id = self.kernel_manager.shell_channel.execute('',
350 silent=True, user_expressions={ local_uuid:expr })
351 silent=True, user_expressions={ local_uuid:expr })
351 self._callback_dict[local_uuid] = callback
352 self._callback_dict[local_uuid] = callback
352 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
353 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
353
354
354 def _handle_exec_callback(self, msg):
355 def _handle_exec_callback(self, msg):
355 """Execute `callback` corresonding to `msg` reply, after ``_silent_exec_callback``
356 """Execute `callback` corresonding to `msg` reply, after ``_silent_exec_callback``
356
357
357 Parameters
358 Parameters
358 ----------
359 ----------
359 msg : raw message send by the kernel containing an `user_expressions`
360 msg : raw message send by the kernel containing an `user_expressions`
360 and having a 'silent_exec_callback' kind.
361 and having a 'silent_exec_callback' kind.
361
362
362 Notes
363 Notes
363 -----
364 -----
364 This fonction will look for a `callback` associated with the
365 This fonction will look for a `callback` associated with the
365 corresponding message id. Association has been made by
366 corresponding message id. Association has been made by
366 `_silent_exec_callback`. `callback` is then called with the `repr()`
367 `_silent_exec_callback`. `callback` is then called with the `repr()`
367 of the value of corresponding `user_expressions` as argument.
368 of the value of corresponding `user_expressions` as argument.
368 `callback` is then removed from the known list so that any message
369 `callback` is then removed from the known list so that any message
369 coming again with the same id won't trigger it.
370 coming again with the same id won't trigger it.
370
371
371 """
372 """
372
373
373 user_exp = msg['content'].get('user_expressions')
374 user_exp = msg['content'].get('user_expressions')
374 if not user_exp:
375 if not user_exp:
375 return
376 return
376 for expression in user_exp:
377 for expression in user_exp:
377 if expression in self._callback_dict:
378 if expression in self._callback_dict:
378 self._callback_dict.pop(expression)(user_exp[expression])
379 self._callback_dict.pop(expression)(user_exp[expression])
379
380
380 def _handle_execute_reply(self, msg):
381 def _handle_execute_reply(self, msg):
381 """ Handles replies for code execution.
382 """ Handles replies for code execution.
382 """
383 """
383 self.log.debug("execute: %s", msg.get('content', ''))
384 self.log.debug("execute: %s", msg.get('content', ''))
384 msg_id = msg['parent_header']['msg_id']
385 msg_id = msg['parent_header']['msg_id']
385 info = self._request_info['execute'].get(msg_id)
386 info = self._request_info['execute'].get(msg_id)
386 # unset reading flag, because if execute finished, raw_input can't
387 # unset reading flag, because if execute finished, raw_input can't
387 # still be pending.
388 # still be pending.
388 self._reading = False
389 self._reading = False
389 if info and info.kind == 'user' and not self._hidden:
390 if info and info.kind == 'user' and not self._hidden:
390 # Make sure that all output from the SUB channel has been processed
391 # Make sure that all output from the SUB channel has been processed
391 # before writing a new prompt.
392 # before writing a new prompt.
392 self.kernel_manager.sub_channel.flush()
393 self.kernel_manager.sub_channel.flush()
393
394
394 # Reset the ANSI style information to prevent bad text in stdout
395 # Reset the ANSI style information to prevent bad text in stdout
395 # from messing up our colors. We're not a true terminal so we're
396 # from messing up our colors. We're not a true terminal so we're
396 # allowed to do this.
397 # allowed to do this.
397 if self.ansi_codes:
398 if self.ansi_codes:
398 self._ansi_processor.reset_sgr()
399 self._ansi_processor.reset_sgr()
399
400
400 content = msg['content']
401 content = msg['content']
401 status = content['status']
402 status = content['status']
402 if status == 'ok':
403 if status == 'ok':
403 self._process_execute_ok(msg)
404 self._process_execute_ok(msg)
404 elif status == 'error':
405 elif status == 'error':
405 self._process_execute_error(msg)
406 self._process_execute_error(msg)
406 elif status == 'aborted':
407 elif status == 'aborted':
407 self._process_execute_abort(msg)
408 self._process_execute_abort(msg)
408
409
409 self._show_interpreter_prompt_for_reply(msg)
410 self._show_interpreter_prompt_for_reply(msg)
410 self.executed.emit(msg)
411 self.executed.emit(msg)
411 self._request_info['execute'].pop(msg_id)
412 self._request_info['execute'].pop(msg_id)
412 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
413 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
413 self._handle_exec_callback(msg)
414 self._handle_exec_callback(msg)
414 self._request_info['execute'].pop(msg_id)
415 self._request_info['execute'].pop(msg_id)
415 else:
416 else:
416 super(FrontendWidget, self)._handle_execute_reply(msg)
417 super(FrontendWidget, self)._handle_execute_reply(msg)
417
418
418 def _handle_input_request(self, msg):
419 def _handle_input_request(self, msg):
419 """ Handle requests for raw_input.
420 """ Handle requests for raw_input.
420 """
421 """
421 self.log.debug("input: %s", msg.get('content', ''))
422 self.log.debug("input: %s", msg.get('content', ''))
422 if self._hidden:
423 if self._hidden:
423 raise RuntimeError('Request for raw input during hidden execution.')
424 raise RuntimeError('Request for raw input during hidden execution.')
424
425
425 # Make sure that all output from the SUB channel has been processed
426 # Make sure that all output from the SUB channel has been processed
426 # before entering readline mode.
427 # before entering readline mode.
427 self.kernel_manager.sub_channel.flush()
428 self.kernel_manager.sub_channel.flush()
428
429
429 def callback(line):
430 def callback(line):
430 self.kernel_manager.stdin_channel.input(line)
431 self.kernel_manager.stdin_channel.input(line)
431 if self._reading:
432 if self._reading:
432 self.log.debug("Got second input request, assuming first was interrupted.")
433 self.log.debug("Got second input request, assuming first was interrupted.")
433 self._reading = False
434 self._reading = False
434 self._readline(msg['content']['prompt'], callback=callback)
435 self._readline(msg['content']['prompt'], callback=callback)
435
436
436 def _handle_kernel_died(self, since_last_heartbeat):
437 def _handle_kernel_died(self, since_last_heartbeat):
437 """ Handle the kernel's death by asking if the user wants to restart.
438 """ Handle the kernel's death by asking if the user wants to restart.
438 """
439 """
439 self.log.debug("kernel died: %s", since_last_heartbeat)
440 self.log.debug("kernel died: %s", since_last_heartbeat)
440 if self.custom_restart:
441 if self.custom_restart:
441 self.custom_restart_kernel_died.emit(since_last_heartbeat)
442 self.custom_restart_kernel_died.emit(since_last_heartbeat)
442 else:
443 else:
443 message = 'The kernel heartbeat has been inactive for %.2f ' \
444 message = 'The kernel heartbeat has been inactive for %.2f ' \
444 'seconds. Do you want to restart the kernel? You may ' \
445 'seconds. Do you want to restart the kernel? You may ' \
445 'first want to check the network connection.' % \
446 'first want to check the network connection.' % \
446 since_last_heartbeat
447 since_last_heartbeat
447 self.restart_kernel(message, now=True)
448 self.restart_kernel(message, now=True)
448
449
449 def _handle_object_info_reply(self, rep):
450 def _handle_object_info_reply(self, rep):
450 """ Handle replies for call tips.
451 """ Handle replies for call tips.
451 """
452 """
452 self.log.debug("oinfo: %s", rep.get('content', ''))
453 self.log.debug("oinfo: %s", rep.get('content', ''))
453 cursor = self._get_cursor()
454 cursor = self._get_cursor()
454 info = self._request_info.get('call_tip')
455 info = self._request_info.get('call_tip')
455 if info and info.id == rep['parent_header']['msg_id'] and \
456 if info and info.id == rep['parent_header']['msg_id'] and \
456 info.pos == cursor.position():
457 info.pos == cursor.position():
457 # Get the information for a call tip. For now we format the call
458 # Get the information for a call tip. For now we format the call
458 # line as string, later we can pass False to format_call and
459 # line as string, later we can pass False to format_call and
459 # syntax-highlight it ourselves for nicer formatting in the
460 # syntax-highlight it ourselves for nicer formatting in the
460 # calltip.
461 # calltip.
461 content = rep['content']
462 content = rep['content']
462 # if this is from pykernel, 'docstring' will be the only key
463 # if this is from pykernel, 'docstring' will be the only key
463 if content.get('ismagic', False):
464 if content.get('ismagic', False):
464 # Don't generate a call-tip for magics. Ideally, we should
465 # Don't generate a call-tip for magics. Ideally, we should
465 # generate a tooltip, but not on ( like we do for actual
466 # generate a tooltip, but not on ( like we do for actual
466 # callables.
467 # callables.
467 call_info, doc = None, None
468 call_info, doc = None, None
468 else:
469 else:
469 call_info, doc = call_tip(content, format_call=True)
470 call_info, doc = call_tip(content, format_call=True)
470 if call_info or doc:
471 if call_info or doc:
471 self._call_tip_widget.show_call_info(call_info, doc)
472 self._call_tip_widget.show_call_info(call_info, doc)
472
473
473 def _handle_pyout(self, msg):
474 def _handle_pyout(self, msg):
474 """ Handle display hook output.
475 """ Handle display hook output.
475 """
476 """
476 self.log.debug("pyout: %s", msg.get('content', ''))
477 self.log.debug("pyout: %s", msg.get('content', ''))
477 if not self._hidden and self._is_from_this_session(msg):
478 if not self._hidden and self._is_from_this_session(msg):
478 text = msg['content']['data']
479 text = msg['content']['data']
479 self._append_plain_text(text + '\n', before_prompt=True)
480 self._append_plain_text(text + '\n', before_prompt=True)
480
481
481 def _handle_stream(self, msg):
482 def _handle_stream(self, msg):
482 """ Handle stdout, stderr, and stdin.
483 """ Handle stdout, stderr, and stdin.
483 """
484 """
484 self.log.debug("stream: %s", msg.get('content', ''))
485 self.log.debug("stream: %s", msg.get('content', ''))
485 if not self._hidden and self._is_from_this_session(msg):
486 if not self._hidden and self._is_from_this_session(msg):
486 # Most consoles treat tabs as being 8 space characters. Convert tabs
487 # Most consoles treat tabs as being 8 space characters. Convert tabs
487 # to spaces so that output looks as expected regardless of this
488 # to spaces so that output looks as expected regardless of this
488 # widget's tab width.
489 # widget's tab width.
489 text = msg['content']['data'].expandtabs(8)
490 text = msg['content']['data'].expandtabs(8)
490
491
491 self._append_plain_text(text, before_prompt=True)
492 self._append_plain_text(text, before_prompt=True)
492 self._control.moveCursor(QtGui.QTextCursor.End)
493 self._control.moveCursor(QtGui.QTextCursor.End)
493
494
494 def _handle_shutdown_reply(self, msg):
495 def _handle_shutdown_reply(self, msg):
495 """ Handle shutdown signal, only if from other console.
496 """ Handle shutdown signal, only if from other console.
496 """
497 """
497 self.log.debug("shutdown: %s", msg.get('content', ''))
498 self.log.debug("shutdown: %s", msg.get('content', ''))
498 if not self._hidden and not self._is_from_this_session(msg):
499 if not self._hidden and not self._is_from_this_session(msg):
499 if self._local_kernel:
500 if self._local_kernel:
500 if not msg['content']['restart']:
501 if not msg['content']['restart']:
501 self.exit_requested.emit(self)
502 self.exit_requested.emit(self)
502 else:
503 else:
503 # we just got notified of a restart!
504 # we just got notified of a restart!
504 time.sleep(0.25) # wait 1/4 sec to reset
505 time.sleep(0.25) # wait 1/4 sec to reset
505 # lest the request for a new prompt
506 # lest the request for a new prompt
506 # goes to the old kernel
507 # goes to the old kernel
507 self.reset()
508 self.reset()
508 else: # remote kernel, prompt on Kernel shutdown/reset
509 else: # remote kernel, prompt on Kernel shutdown/reset
509 title = self.window().windowTitle()
510 title = self.window().windowTitle()
510 if not msg['content']['restart']:
511 if not msg['content']['restart']:
511 reply = QtGui.QMessageBox.question(self, title,
512 reply = QtGui.QMessageBox.question(self, title,
512 "Kernel has been shutdown permanently. "
513 "Kernel has been shutdown permanently. "
513 "Close the Console?",
514 "Close the Console?",
514 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
515 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
515 if reply == QtGui.QMessageBox.Yes:
516 if reply == QtGui.QMessageBox.Yes:
516 self.exit_requested.emit(self)
517 self.exit_requested.emit(self)
517 else:
518 else:
518 reply = QtGui.QMessageBox.question(self, title,
519 reply = QtGui.QMessageBox.question(self, title,
519 "Kernel has been reset. Clear the Console?",
520 "Kernel has been reset. Clear the Console?",
520 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
521 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
521 if reply == QtGui.QMessageBox.Yes:
522 if reply == QtGui.QMessageBox.Yes:
522 time.sleep(0.25) # wait 1/4 sec to reset
523 time.sleep(0.25) # wait 1/4 sec to reset
523 # lest the request for a new prompt
524 # lest the request for a new prompt
524 # goes to the old kernel
525 # goes to the old kernel
525 self.reset()
526 self.reset()
526
527
527 def _started_channels(self):
528 def _started_channels(self):
528 """ Called when the KernelManager channels have started listening or
529 """ Called when the KernelManager channels have started listening or
529 when the frontend is assigned an already listening KernelManager.
530 when the frontend is assigned an already listening KernelManager.
530 """
531 """
531 self.reset()
532 self.reset()
532
533
533 #---------------------------------------------------------------------------
534 #---------------------------------------------------------------------------
534 # 'FrontendWidget' public interface
535 # 'FrontendWidget' public interface
535 #---------------------------------------------------------------------------
536 #---------------------------------------------------------------------------
536
537
537 def copy_raw(self):
538 def copy_raw(self):
538 """ Copy the currently selected text to the clipboard without attempting
539 """ Copy the currently selected text to the clipboard without attempting
539 to remove prompts or otherwise alter the text.
540 to remove prompts or otherwise alter the text.
540 """
541 """
541 self._control.copy()
542 self._control.copy()
542
543
543 def execute_file(self, path, hidden=False):
544 def execute_file(self, path, hidden=False):
544 """ Attempts to execute file with 'path'. If 'hidden', no output is
545 """ Attempts to execute file with 'path'. If 'hidden', no output is
545 shown.
546 shown.
546 """
547 """
547 self.execute('execfile(%r)' % path, hidden=hidden)
548 self.execute('execfile(%r)' % path, hidden=hidden)
548
549
549 def interrupt_kernel(self):
550 def interrupt_kernel(self):
550 """ Attempts to interrupt the running kernel.
551 """ Attempts to interrupt the running kernel.
551
552
552 Also unsets _reading flag, to avoid runtime errors
553 Also unsets _reading flag, to avoid runtime errors
553 if raw_input is called again.
554 if raw_input is called again.
554 """
555 """
555 if self.custom_interrupt:
556 if self.custom_interrupt:
556 self._reading = False
557 self._reading = False
557 self.custom_interrupt_requested.emit()
558 self.custom_interrupt_requested.emit()
558 elif self.kernel_manager.has_kernel:
559 elif self.kernel_manager.has_kernel:
559 self._reading = False
560 self._reading = False
560 self.kernel_manager.interrupt_kernel()
561 self.kernel_manager.interrupt_kernel()
561 else:
562 else:
562 self._append_plain_text('Kernel process is either remote or '
563 self._append_plain_text('Kernel process is either remote or '
563 'unspecified. Cannot interrupt.\n')
564 'unspecified. Cannot interrupt.\n')
564
565
565 def reset(self):
566 def reset(self):
566 """ Resets the widget to its initial state. Similar to ``clear``, but
567 """ Resets the widget to its initial state. Similar to ``clear``, but
567 also re-writes the banner and aborts execution if necessary.
568 also re-writes the banner and aborts execution if necessary.
568 """
569 """
569 if self._executing:
570 if self._executing:
570 self._executing = False
571 self._executing = False
571 self._request_info['execute'] = {}
572 self._request_info['execute'] = {}
572 self._reading = False
573 self._reading = False
573 self._highlighter.highlighting_on = False
574 self._highlighter.highlighting_on = False
574
575
575 self._control.clear()
576 self._control.clear()
576 self._append_plain_text(self.banner)
577 self._append_plain_text(self.banner)
577 # update output marker for stdout/stderr, so that startup
578 # update output marker for stdout/stderr, so that startup
578 # messages appear after banner:
579 # messages appear after banner:
579 self._append_before_prompt_pos = self._get_cursor().position()
580 self._append_before_prompt_pos = self._get_cursor().position()
580 self._show_interpreter_prompt()
581 self._show_interpreter_prompt()
581
582
582 def restart_kernel(self, message, now=False):
583 def restart_kernel(self, message, now=False):
583 """ Attempts to restart the running kernel.
584 """ Attempts to restart the running kernel.
584 """
585 """
585 # FIXME: now should be configurable via a checkbox in the dialog. Right
586 # FIXME: now should be configurable via a checkbox in the dialog. Right
586 # now at least the heartbeat path sets it to True and the manual restart
587 # now at least the heartbeat path sets it to True and the manual restart
587 # to False. But those should just be the pre-selected states of a
588 # to False. But those should just be the pre-selected states of a
588 # checkbox that the user could override if so desired. But I don't know
589 # checkbox that the user could override if so desired. But I don't know
589 # enough Qt to go implementing the checkbox now.
590 # enough Qt to go implementing the checkbox now.
590
591
591 if self.custom_restart:
592 if self.custom_restart:
592 self.custom_restart_requested.emit()
593 self.custom_restart_requested.emit()
593
594
594 elif self.kernel_manager.has_kernel:
595 elif self.kernel_manager.has_kernel:
595 # Pause the heart beat channel to prevent further warnings.
596 # Pause the heart beat channel to prevent further warnings.
596 self.kernel_manager.hb_channel.pause()
597 self.kernel_manager.hb_channel.pause()
597
598
598 # Prompt the user to restart the kernel. Un-pause the heartbeat if
599 # Prompt the user to restart the kernel. Un-pause the heartbeat if
599 # they decline. (If they accept, the heartbeat will be un-paused
600 # they decline. (If they accept, the heartbeat will be un-paused
600 # automatically when the kernel is restarted.)
601 # automatically when the kernel is restarted.)
601 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
602 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
602 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
603 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
603 message, buttons)
604 message, buttons)
604 if result == QtGui.QMessageBox.Yes:
605 if result == QtGui.QMessageBox.Yes:
605 try:
606 try:
606 self.kernel_manager.restart_kernel(now=now)
607 self.kernel_manager.restart_kernel(now=now)
607 except RuntimeError:
608 except RuntimeError:
608 self._append_plain_text('Kernel started externally. '
609 self._append_plain_text('Kernel started externally. '
609 'Cannot restart.\n')
610 'Cannot restart.\n')
610 else:
611 else:
611 self.reset()
612 self.reset()
612 else:
613 else:
613 self.kernel_manager.hb_channel.unpause()
614 self.kernel_manager.hb_channel.unpause()
614
615
615 else:
616 else:
616 self._append_plain_text('Kernel process is either remote or '
617 self._append_plain_text('Kernel process is either remote or '
617 'unspecified. Cannot restart.\n')
618 'unspecified. Cannot restart.\n')
618
619
619 #---------------------------------------------------------------------------
620 #---------------------------------------------------------------------------
620 # 'FrontendWidget' protected interface
621 # 'FrontendWidget' protected interface
621 #---------------------------------------------------------------------------
622 #---------------------------------------------------------------------------
622
623
623 def _call_tip(self):
624 def _call_tip(self):
624 """ Shows a call tip, if appropriate, at the current cursor location.
625 """ Shows a call tip, if appropriate, at the current cursor location.
625 """
626 """
626 # Decide if it makes sense to show a call tip
627 # Decide if it makes sense to show a call tip
627 if not self.enable_calltips:
628 if not self.enable_calltips:
628 return False
629 return False
629 cursor = self._get_cursor()
630 cursor = self._get_cursor()
630 cursor.movePosition(QtGui.QTextCursor.Left)
631 cursor.movePosition(QtGui.QTextCursor.Left)
631 if cursor.document().characterAt(cursor.position()) != '(':
632 if cursor.document().characterAt(cursor.position()) != '(':
632 return False
633 return False
633 context = self._get_context(cursor)
634 context = self._get_context(cursor)
634 if not context:
635 if not context:
635 return False
636 return False
636
637
637 # Send the metadata request to the kernel
638 # Send the metadata request to the kernel
638 name = '.'.join(context)
639 name = '.'.join(context)
639 msg_id = self.kernel_manager.shell_channel.object_info(name)
640 msg_id = self.kernel_manager.shell_channel.object_info(name)
640 pos = self._get_cursor().position()
641 pos = self._get_cursor().position()
641 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
642 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
642 return True
643 return True
643
644
644 def _complete(self):
645 def _complete(self):
645 """ Performs completion at the current cursor location.
646 """ Performs completion at the current cursor location.
646 """
647 """
647 context = self._get_context()
648 context = self._get_context()
648 if context:
649 if context:
649 # Send the completion request to the kernel
650 # Send the completion request to the kernel
650 msg_id = self.kernel_manager.shell_channel.complete(
651 msg_id = self.kernel_manager.shell_channel.complete(
651 '.'.join(context), # text
652 '.'.join(context), # text
652 self._get_input_buffer_cursor_line(), # line
653 self._get_input_buffer_cursor_line(), # line
653 self._get_input_buffer_cursor_column(), # cursor_pos
654 self._get_input_buffer_cursor_column(), # cursor_pos
654 self.input_buffer) # block
655 self.input_buffer) # block
655 pos = self._get_cursor().position()
656 pos = self._get_cursor().position()
656 info = self._CompletionRequest(msg_id, pos)
657 info = self._CompletionRequest(msg_id, pos)
657 self._request_info['complete'] = info
658 self._request_info['complete'] = info
658
659
659 def _get_context(self, cursor=None):
660 def _get_context(self, cursor=None):
660 """ Gets the context for the specified cursor (or the current cursor
661 """ Gets the context for the specified cursor (or the current cursor
661 if none is specified).
662 if none is specified).
662 """
663 """
663 if cursor is None:
664 if cursor is None:
664 cursor = self._get_cursor()
665 cursor = self._get_cursor()
665 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
666 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
666 QtGui.QTextCursor.KeepAnchor)
667 QtGui.QTextCursor.KeepAnchor)
667 text = cursor.selection().toPlainText()
668 text = cursor.selection().toPlainText()
668 return self._completion_lexer.get_context(text)
669 return self._completion_lexer.get_context(text)
669
670
670 def _process_execute_abort(self, msg):
671 def _process_execute_abort(self, msg):
671 """ Process a reply for an aborted execution request.
672 """ Process a reply for an aborted execution request.
672 """
673 """
673 self._append_plain_text("ERROR: execution aborted\n")
674 self._append_plain_text("ERROR: execution aborted\n")
674
675
675 def _process_execute_error(self, msg):
676 def _process_execute_error(self, msg):
676 """ Process a reply for an execution request that resulted in an error.
677 """ Process a reply for an execution request that resulted in an error.
677 """
678 """
678 content = msg['content']
679 content = msg['content']
679 # If a SystemExit is passed along, this means exit() was called - also
680 # If a SystemExit is passed along, this means exit() was called - also
680 # all the ipython %exit magic syntax of '-k' to be used to keep
681 # all the ipython %exit magic syntax of '-k' to be used to keep
681 # the kernel running
682 # the kernel running
682 if content['ename']=='SystemExit':
683 if content['ename']=='SystemExit':
683 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
684 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
684 self._keep_kernel_on_exit = keepkernel
685 self._keep_kernel_on_exit = keepkernel
685 self.exit_requested.emit(self)
686 self.exit_requested.emit(self)
686 else:
687 else:
687 traceback = ''.join(content['traceback'])
688 traceback = ''.join(content['traceback'])
688 self._append_plain_text(traceback)
689 self._append_plain_text(traceback)
689
690
690 def _process_execute_ok(self, msg):
691 def _process_execute_ok(self, msg):
691 """ Process a reply for a successful execution equest.
692 """ Process a reply for a successful execution equest.
692 """
693 """
693 payload = msg['content']['payload']
694 payload = msg['content']['payload']
694 for item in payload:
695 for item in payload:
695 if not self._process_execute_payload(item):
696 if not self._process_execute_payload(item):
696 warning = 'Warning: received unknown payload of type %s'
697 warning = 'Warning: received unknown payload of type %s'
697 print(warning % repr(item['source']))
698 print(warning % repr(item['source']))
698
699
699 def _process_execute_payload(self, item):
700 def _process_execute_payload(self, item):
700 """ Process a single payload item from the list of payload items in an
701 """ Process a single payload item from the list of payload items in an
701 execution reply. Returns whether the payload was handled.
702 execution reply. Returns whether the payload was handled.
702 """
703 """
703 # The basic FrontendWidget doesn't handle payloads, as they are a
704 # The basic FrontendWidget doesn't handle payloads, as they are a
704 # mechanism for going beyond the standard Python interpreter model.
705 # mechanism for going beyond the standard Python interpreter model.
705 return False
706 return False
706
707
707 def _show_interpreter_prompt(self):
708 def _show_interpreter_prompt(self):
708 """ Shows a prompt for the interpreter.
709 """ Shows a prompt for the interpreter.
709 """
710 """
710 self._show_prompt('>>> ')
711 self._show_prompt('>>> ')
711
712
712 def _show_interpreter_prompt_for_reply(self, msg):
713 def _show_interpreter_prompt_for_reply(self, msg):
713 """ Shows a prompt for the interpreter given an 'execute_reply' message.
714 """ Shows a prompt for the interpreter given an 'execute_reply' message.
714 """
715 """
715 self._show_interpreter_prompt()
716 self._show_interpreter_prompt()
716
717
717 #------ Signal handlers ----------------------------------------------------
718 #------ Signal handlers ----------------------------------------------------
718
719
719 def _document_contents_change(self, position, removed, added):
720 def _document_contents_change(self, position, removed, added):
720 """ Called whenever the document's content changes. Display a call tip
721 """ Called whenever the document's content changes. Display a call tip
721 if appropriate.
722 if appropriate.
722 """
723 """
723 # Calculate where the cursor should be *after* the change:
724 # Calculate where the cursor should be *after* the change:
724 position += added
725 position += added
725
726
726 document = self._control.document()
727 document = self._control.document()
727 if position == self._get_cursor().position():
728 if position == self._get_cursor().position():
728 self._call_tip()
729 self._call_tip()
729
730
730 #------ Trait default initializers -----------------------------------------
731 #------ Trait default initializers -----------------------------------------
731
732
732 def _banner_default(self):
733 def _banner_default(self):
733 """ Returns the standard Python banner.
734 """ Returns the standard Python banner.
734 """
735 """
735 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
736 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
736 '"license" for more information.'
737 '"license" for more information.'
737 return banner % (sys.version, sys.platform)
738 return banner % (sys.version, sys.platform)
@@ -1,569 +1,554 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
4
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Imports
6 # Imports
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard library imports
9 # Standard library imports
10 from collections import namedtuple
10 from collections import namedtuple
11 import os.path
11 import os.path
12 import re
12 import re
13 from subprocess import Popen
13 from subprocess import Popen
14 import sys
14 import sys
15 import time
15 import time
16 from textwrap import dedent
16 from textwrap import dedent
17
17
18 # System library imports
18 # System library imports
19 from IPython.external.qt import QtCore, QtGui
19 from IPython.external.qt 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 transform_ipy_prompt
23 transform_ipy_prompt
24 from IPython.utils.traitlets import Bool, Unicode
24 from IPython.utils.traitlets import Bool, Unicode
25 from frontend_widget import FrontendWidget
25 from frontend_widget import FrontendWidget
26 import styles
26 import styles
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Constants
29 # Constants
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 # Default strings to build and display input and output prompts (and separators
32 # Default strings to build and display input and output prompts (and separators
33 # in between)
33 # in between)
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 default_input_sep = '\n'
36 default_input_sep = '\n'
37 default_output_sep = ''
37 default_output_sep = ''
38 default_output_sep2 = ''
38 default_output_sep2 = ''
39
39
40 # Base path for most payload sources.
40 # Base path for most payload sources.
41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42
42
43 if sys.platform.startswith('win'):
43 if sys.platform.startswith('win'):
44 default_editor = 'notepad'
44 default_editor = 'notepad'
45 else:
45 else:
46 default_editor = ''
46 default_editor = ''
47
47
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 # IPythonWidget class
49 # IPythonWidget class
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51
51
52 class IPythonWidget(FrontendWidget):
52 class IPythonWidget(FrontendWidget):
53 """ A FrontendWidget for an IPython kernel.
53 """ A FrontendWidget for an IPython kernel.
54 """
54 """
55
55
56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 # settings.
58 # settings.
59 custom_edit = Bool(False)
59 custom_edit = Bool(False)
60 custom_edit_requested = QtCore.Signal(object, object)
60 custom_edit_requested = QtCore.Signal(object, object)
61
61
62 editor = Unicode(default_editor, config=True,
62 editor = Unicode(default_editor, config=True,
63 help="""
63 help="""
64 A command for invoking a system text editor. If the string contains a
64 A command for invoking a system text editor. If the string contains a
65 {filename} format specifier, it will be used. Otherwise, the filename
65 {filename} format specifier, it will be used. Otherwise, the filename
66 will be appended to the end the command.
66 will be appended to the end the command.
67 """)
67 """)
68
68
69 editor_line = Unicode(config=True,
69 editor_line = Unicode(config=True,
70 help="""
70 help="""
71 The editor command to use when a specific line number is requested. The
71 The editor command to use when a specific line number is requested. The
72 string should contain two format specifiers: {line} and {filename}. If
72 string should contain two format specifiers: {line} and {filename}. If
73 this parameter is not specified, the line number option to the %edit
73 this parameter is not specified, the line number option to the %edit
74 magic will be ignored.
74 magic will be ignored.
75 """)
75 """)
76
76
77 style_sheet = Unicode(config=True,
77 style_sheet = Unicode(config=True,
78 help="""
78 help="""
79 A CSS stylesheet. The stylesheet can contain classes for:
79 A CSS stylesheet. The stylesheet can contain classes for:
80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 3. IPython: .error, .in-prompt, .out-prompt, etc
82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 """)
83 """)
84
84
85 syntax_style = Unicode(config=True,
85 syntax_style = Unicode(config=True,
86 help="""
86 help="""
87 If not empty, use this Pygments style for syntax highlighting.
87 If not empty, use this Pygments style for syntax highlighting.
88 Otherwise, the style sheet is queried for Pygments style
88 Otherwise, the style sheet is queried for Pygments style
89 information.
89 information.
90 """)
90 """)
91
91
92 # Prompts.
92 # Prompts.
93 in_prompt = Unicode(default_in_prompt, config=True)
93 in_prompt = Unicode(default_in_prompt, config=True)
94 out_prompt = Unicode(default_out_prompt, config=True)
94 out_prompt = Unicode(default_out_prompt, config=True)
95 input_sep = Unicode(default_input_sep, config=True)
95 input_sep = Unicode(default_input_sep, config=True)
96 output_sep = Unicode(default_output_sep, config=True)
96 output_sep = Unicode(default_output_sep, config=True)
97 output_sep2 = Unicode(default_output_sep2, config=True)
97 output_sep2 = Unicode(default_output_sep2, config=True)
98
98
99 # FrontendWidget protected class variables.
99 # FrontendWidget protected class variables.
100 _input_splitter_class = IPythonInputSplitter
100 _input_splitter_class = IPythonInputSplitter
101 _transform_prompt = staticmethod(transform_ipy_prompt)
101
102
102 # IPythonWidget protected class variables.
103 # IPythonWidget protected class variables.
103 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 _payload_source_page = 'IPython.zmq.page.page'
108 _payload_source_page = 'IPython.zmq.page.page'
108 _retrying_history_request = False
109 _retrying_history_request = False
109
110
110 #---------------------------------------------------------------------------
111 #---------------------------------------------------------------------------
111 # 'object' interface
112 # 'object' interface
112 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
113
114
114 def __init__(self, *args, **kw):
115 def __init__(self, *args, **kw):
115 super(IPythonWidget, self).__init__(*args, **kw)
116 super(IPythonWidget, self).__init__(*args, **kw)
116
117
117 # IPythonWidget protected variables.
118 # IPythonWidget protected variables.
118 self._payload_handlers = {
119 self._payload_handlers = {
119 self._payload_source_edit : self._handle_payload_edit,
120 self._payload_source_edit : self._handle_payload_edit,
120 self._payload_source_exit : self._handle_payload_exit,
121 self._payload_source_exit : self._handle_payload_exit,
121 self._payload_source_page : self._handle_payload_page,
122 self._payload_source_page : self._handle_payload_page,
122 self._payload_source_next_input : self._handle_payload_next_input }
123 self._payload_source_next_input : self._handle_payload_next_input }
123 self._previous_prompt_obj = None
124 self._previous_prompt_obj = None
124 self._keep_kernel_on_exit = None
125 self._keep_kernel_on_exit = None
125
126
126 # Initialize widget styling.
127 # Initialize widget styling.
127 if self.style_sheet:
128 if self.style_sheet:
128 self._style_sheet_changed()
129 self._style_sheet_changed()
129 self._syntax_style_changed()
130 self._syntax_style_changed()
130 else:
131 else:
131 self.set_default_style()
132 self.set_default_style()
132
133
133 #---------------------------------------------------------------------------
134 #---------------------------------------------------------------------------
134 # 'BaseFrontendMixin' abstract interface
135 # 'BaseFrontendMixin' abstract interface
135 #---------------------------------------------------------------------------
136 #---------------------------------------------------------------------------
136
137
137 def _handle_complete_reply(self, rep):
138 def _handle_complete_reply(self, rep):
138 """ Reimplemented to support IPython's improved completion machinery.
139 """ Reimplemented to support IPython's improved completion machinery.
139 """
140 """
140 self.log.debug("complete: %s", rep.get('content', ''))
141 self.log.debug("complete: %s", rep.get('content', ''))
141 cursor = self._get_cursor()
142 cursor = self._get_cursor()
142 info = self._request_info.get('complete')
143 info = self._request_info.get('complete')
143 if info and info.id == rep['parent_header']['msg_id'] and \
144 if info and info.id == rep['parent_header']['msg_id'] and \
144 info.pos == cursor.position():
145 info.pos == cursor.position():
145 matches = rep['content']['matches']
146 matches = rep['content']['matches']
146 text = rep['content']['matched_text']
147 text = rep['content']['matched_text']
147 offset = len(text)
148 offset = len(text)
148
149
149 # Clean up matches with period and path separators if the matched
150 # Clean up matches with period and path separators if the matched
150 # text has not been transformed. This is done by truncating all
151 # text has not been transformed. This is done by truncating all
151 # but the last component and then suitably decreasing the offset
152 # but the last component and then suitably decreasing the offset
152 # between the current cursor position and the start of completion.
153 # between the current cursor position and the start of completion.
153 if len(matches) > 1 and matches[0][:offset] == text:
154 if len(matches) > 1 and matches[0][:offset] == text:
154 parts = re.split(r'[./\\]', text)
155 parts = re.split(r'[./\\]', text)
155 sep_count = len(parts) - 1
156 sep_count = len(parts) - 1
156 if sep_count:
157 if sep_count:
157 chop_length = sum(map(len, parts[:sep_count])) + sep_count
158 chop_length = sum(map(len, parts[:sep_count])) + sep_count
158 matches = [ match[chop_length:] for match in matches ]
159 matches = [ match[chop_length:] for match in matches ]
159 offset -= chop_length
160 offset -= chop_length
160
161
161 # Move the cursor to the start of the match and complete.
162 # Move the cursor to the start of the match and complete.
162 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
163 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
163 self._complete_with_items(cursor, matches)
164 self._complete_with_items(cursor, matches)
164
165
165 def _handle_execute_reply(self, msg):
166 def _handle_execute_reply(self, msg):
166 """ Reimplemented to support prompt requests.
167 """ Reimplemented to support prompt requests.
167 """
168 """
168 msg_id = msg['parent_header'].get('msg_id')
169 msg_id = msg['parent_header'].get('msg_id')
169 info = self._request_info['execute'].get(msg_id)
170 info = self._request_info['execute'].get(msg_id)
170 if info and info.kind == 'prompt':
171 if info and info.kind == 'prompt':
171 number = msg['content']['execution_count'] + 1
172 number = msg['content']['execution_count'] + 1
172 self._show_interpreter_prompt(number)
173 self._show_interpreter_prompt(number)
173 self._request_info['execute'].pop(msg_id)
174 self._request_info['execute'].pop(msg_id)
174 else:
175 else:
175 super(IPythonWidget, self)._handle_execute_reply(msg)
176 super(IPythonWidget, self)._handle_execute_reply(msg)
176
177
177 def _handle_history_reply(self, msg):
178 def _handle_history_reply(self, msg):
178 """ Implemented to handle history tail replies, which are only supported
179 """ Implemented to handle history tail replies, which are only supported
179 by the IPython kernel.
180 by the IPython kernel.
180 """
181 """
181 self.log.debug("history: %s", msg.get('content', ''))
182 self.log.debug("history: %s", msg.get('content', ''))
182 content = msg['content']
183 content = msg['content']
183 if 'history' not in content:
184 if 'history' not in content:
184 self.log.error("History request failed: %r"%content)
185 self.log.error("History request failed: %r"%content)
185 if content.get('status', '') == 'aborted' and \
186 if content.get('status', '') == 'aborted' and \
186 not self._retrying_history_request:
187 not self._retrying_history_request:
187 # a *different* action caused this request to be aborted, so
188 # a *different* action caused this request to be aborted, so
188 # we should try again.
189 # we should try again.
189 self.log.error("Retrying aborted history request")
190 self.log.error("Retrying aborted history request")
190 # prevent multiple retries of aborted requests:
191 # prevent multiple retries of aborted requests:
191 self._retrying_history_request = True
192 self._retrying_history_request = True
192 # wait out the kernel's queue flush, which is currently timed at 0.1s
193 # wait out the kernel's queue flush, which is currently timed at 0.1s
193 time.sleep(0.25)
194 time.sleep(0.25)
194 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
195 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
195 else:
196 else:
196 self._retrying_history_request = False
197 self._retrying_history_request = False
197 return
198 return
198 # reset retry flag
199 # reset retry flag
199 self._retrying_history_request = False
200 self._retrying_history_request = False
200 history_items = content['history']
201 history_items = content['history']
201 items = []
202 items = []
202 last_cell = u""
203 last_cell = u""
203 for _, _, cell in history_items:
204 for _, _, cell in history_items:
204 cell = cell.rstrip()
205 cell = cell.rstrip()
205 if cell != last_cell:
206 if cell != last_cell:
206 items.append(cell)
207 items.append(cell)
207 last_cell = cell
208 last_cell = cell
208 self._set_history(items)
209 self._set_history(items)
209
210
210 def _handle_pyout(self, msg):
211 def _handle_pyout(self, msg):
211 """ Reimplemented for IPython-style "display hook".
212 """ Reimplemented for IPython-style "display hook".
212 """
213 """
213 self.log.debug("pyout: %s", msg.get('content', ''))
214 self.log.debug("pyout: %s", msg.get('content', ''))
214 if not self._hidden and self._is_from_this_session(msg):
215 if not self._hidden and self._is_from_this_session(msg):
215 content = msg['content']
216 content = msg['content']
216 prompt_number = content['execution_count']
217 prompt_number = content['execution_count']
217 data = content['data']
218 data = content['data']
218 if data.has_key('text/html'):
219 if data.has_key('text/html'):
219 self._append_plain_text(self.output_sep, True)
220 self._append_plain_text(self.output_sep, True)
220 self._append_html(self._make_out_prompt(prompt_number), True)
221 self._append_html(self._make_out_prompt(prompt_number), True)
221 html = data['text/html']
222 html = data['text/html']
222 self._append_plain_text('\n', True)
223 self._append_plain_text('\n', True)
223 self._append_html(html + self.output_sep2, True)
224 self._append_html(html + self.output_sep2, True)
224 elif data.has_key('text/plain'):
225 elif data.has_key('text/plain'):
225 self._append_plain_text(self.output_sep, True)
226 self._append_plain_text(self.output_sep, True)
226 self._append_html(self._make_out_prompt(prompt_number), True)
227 self._append_html(self._make_out_prompt(prompt_number), True)
227 text = data['text/plain']
228 text = data['text/plain']
228 # If the repr is multiline, make sure we start on a new line,
229 # If the repr is multiline, make sure we start on a new line,
229 # so that its lines are aligned.
230 # so that its lines are aligned.
230 if "\n" in text and not self.output_sep.endswith("\n"):
231 if "\n" in text and not self.output_sep.endswith("\n"):
231 self._append_plain_text('\n', True)
232 self._append_plain_text('\n', True)
232 self._append_plain_text(text + self.output_sep2, True)
233 self._append_plain_text(text + self.output_sep2, True)
233
234
234 def _handle_display_data(self, msg):
235 def _handle_display_data(self, msg):
235 """ The base handler for the ``display_data`` message.
236 """ The base handler for the ``display_data`` message.
236 """
237 """
237 self.log.debug("display: %s", msg.get('content', ''))
238 self.log.debug("display: %s", msg.get('content', ''))
238 # For now, we don't display data from other frontends, but we
239 # For now, we don't display data from other frontends, but we
239 # eventually will as this allows all frontends to monitor the display
240 # eventually will as this allows all frontends to monitor the display
240 # data. But we need to figure out how to handle this in the GUI.
241 # data. But we need to figure out how to handle this in the GUI.
241 if not self._hidden and self._is_from_this_session(msg):
242 if not self._hidden and self._is_from_this_session(msg):
242 source = msg['content']['source']
243 source = msg['content']['source']
243 data = msg['content']['data']
244 data = msg['content']['data']
244 metadata = msg['content']['metadata']
245 metadata = msg['content']['metadata']
245 # In the regular IPythonWidget, we simply print the plain text
246 # In the regular IPythonWidget, we simply print the plain text
246 # representation.
247 # representation.
247 if data.has_key('text/html'):
248 if data.has_key('text/html'):
248 html = data['text/html']
249 html = data['text/html']
249 self._append_html(html, True)
250 self._append_html(html, True)
250 elif data.has_key('text/plain'):
251 elif data.has_key('text/plain'):
251 text = data['text/plain']
252 text = data['text/plain']
252 self._append_plain_text(text, True)
253 self._append_plain_text(text, True)
253 # This newline seems to be needed for text and html output.
254 # This newline seems to be needed for text and html output.
254 self._append_plain_text(u'\n', True)
255 self._append_plain_text(u'\n', True)
255
256
256 def _started_channels(self):
257 def _started_channels(self):
257 """ Reimplemented to make a history request.
258 """ Reimplemented to make a history request.
258 """
259 """
259 super(IPythonWidget, self)._started_channels()
260 super(IPythonWidget, self)._started_channels()
260 self.kernel_manager.shell_channel.history(hist_access_type='tail',
261 self.kernel_manager.shell_channel.history(hist_access_type='tail',
261 n=1000)
262 n=1000)
262 #---------------------------------------------------------------------------
263 #---------------------------------------------------------------------------
263 # 'ConsoleWidget' public interface
264 # 'ConsoleWidget' public interface
264 #---------------------------------------------------------------------------
265 #---------------------------------------------------------------------------
265
266
266 def copy(self):
267 """ Copy the currently selected text to the clipboard, removing prompts
268 if possible.
269 """
270 if self._page_control.hasFocus() :
271 self._page_control.copy()
272 elif self._control.hasFocus() :
273 text = self._control.textCursor().selection().toPlainText()
274 if text:
275 lines = map(transform_ipy_prompt, text.splitlines())
276 text = '\n'.join(lines)
277 QtGui.QApplication.clipboard().setText(text)
278 else :
279 self.log.debug("ipython_widget : unknown copy taget")
280
281
282 #---------------------------------------------------------------------------
267 #---------------------------------------------------------------------------
283 # 'FrontendWidget' public interface
268 # 'FrontendWidget' public interface
284 #---------------------------------------------------------------------------
269 #---------------------------------------------------------------------------
285
270
286 def execute_file(self, path, hidden=False):
271 def execute_file(self, path, hidden=False):
287 """ Reimplemented to use the 'run' magic.
272 """ Reimplemented to use the 'run' magic.
288 """
273 """
289 # Use forward slashes on Windows to avoid escaping each separator.
274 # Use forward slashes on Windows to avoid escaping each separator.
290 if sys.platform == 'win32':
275 if sys.platform == 'win32':
291 path = os.path.normpath(path).replace('\\', '/')
276 path = os.path.normpath(path).replace('\\', '/')
292
277
293 # Perhaps we should not be using %run directly, but while we
278 # Perhaps we should not be using %run directly, but while we
294 # are, it is necessary to quote filenames containing spaces or quotes.
279 # are, it is necessary to quote filenames containing spaces or quotes.
295 # Escaping quotes in filename in %run seems tricky and inconsistent,
280 # Escaping quotes in filename in %run seems tricky and inconsistent,
296 # so not trying it at present.
281 # so not trying it at present.
297 if '"' in path:
282 if '"' in path:
298 if "'" in path:
283 if "'" in path:
299 raise ValueError("Can't run filename containing both single "
284 raise ValueError("Can't run filename containing both single "
300 "and double quotes: %s" % path)
285 "and double quotes: %s" % path)
301 path = "'%s'" % path
286 path = "'%s'" % path
302 elif ' ' in path or "'" in path:
287 elif ' ' in path or "'" in path:
303 path = '"%s"' % path
288 path = '"%s"' % path
304
289
305 self.execute('%%run %s' % path, hidden=hidden)
290 self.execute('%%run %s' % path, hidden=hidden)
306
291
307 #---------------------------------------------------------------------------
292 #---------------------------------------------------------------------------
308 # 'FrontendWidget' protected interface
293 # 'FrontendWidget' protected interface
309 #---------------------------------------------------------------------------
294 #---------------------------------------------------------------------------
310
295
311 def _complete(self):
296 def _complete(self):
312 """ Reimplemented to support IPython's improved completion machinery.
297 """ Reimplemented to support IPython's improved completion machinery.
313 """
298 """
314 # We let the kernel split the input line, so we *always* send an empty
299 # We let the kernel split the input line, so we *always* send an empty
315 # text field. Readline-based frontends do get a real text field which
300 # text field. Readline-based frontends do get a real text field which
316 # they can use.
301 # they can use.
317 text = ''
302 text = ''
318
303
319 # Send the completion request to the kernel
304 # Send the completion request to the kernel
320 msg_id = self.kernel_manager.shell_channel.complete(
305 msg_id = self.kernel_manager.shell_channel.complete(
321 text, # text
306 text, # text
322 self._get_input_buffer_cursor_line(), # line
307 self._get_input_buffer_cursor_line(), # line
323 self._get_input_buffer_cursor_column(), # cursor_pos
308 self._get_input_buffer_cursor_column(), # cursor_pos
324 self.input_buffer) # block
309 self.input_buffer) # block
325 pos = self._get_cursor().position()
310 pos = self._get_cursor().position()
326 info = self._CompletionRequest(msg_id, pos)
311 info = self._CompletionRequest(msg_id, pos)
327 self._request_info['complete'] = info
312 self._request_info['complete'] = info
328
313
329 def _process_execute_error(self, msg):
314 def _process_execute_error(self, msg):
330 """ Reimplemented for IPython-style traceback formatting.
315 """ Reimplemented for IPython-style traceback formatting.
331 """
316 """
332 content = msg['content']
317 content = msg['content']
333 traceback = '\n'.join(content['traceback']) + '\n'
318 traceback = '\n'.join(content['traceback']) + '\n'
334 if False:
319 if False:
335 # FIXME: For now, tracebacks come as plain text, so we can't use
320 # FIXME: For now, tracebacks come as plain text, so we can't use
336 # the html renderer yet. Once we refactor ultratb to produce
321 # the html renderer yet. Once we refactor ultratb to produce
337 # properly styled tracebacks, this branch should be the default
322 # properly styled tracebacks, this branch should be the default
338 traceback = traceback.replace(' ', '&nbsp;')
323 traceback = traceback.replace(' ', '&nbsp;')
339 traceback = traceback.replace('\n', '<br/>')
324 traceback = traceback.replace('\n', '<br/>')
340
325
341 ename = content['ename']
326 ename = content['ename']
342 ename_styled = '<span class="error">%s</span>' % ename
327 ename_styled = '<span class="error">%s</span>' % ename
343 traceback = traceback.replace(ename, ename_styled)
328 traceback = traceback.replace(ename, ename_styled)
344
329
345 self._append_html(traceback)
330 self._append_html(traceback)
346 else:
331 else:
347 # This is the fallback for now, using plain text with ansi escapes
332 # This is the fallback for now, using plain text with ansi escapes
348 self._append_plain_text(traceback)
333 self._append_plain_text(traceback)
349
334
350 def _process_execute_payload(self, item):
335 def _process_execute_payload(self, item):
351 """ Reimplemented to dispatch payloads to handler methods.
336 """ Reimplemented to dispatch payloads to handler methods.
352 """
337 """
353 handler = self._payload_handlers.get(item['source'])
338 handler = self._payload_handlers.get(item['source'])
354 if handler is None:
339 if handler is None:
355 # We have no handler for this type of payload, simply ignore it
340 # We have no handler for this type of payload, simply ignore it
356 return False
341 return False
357 else:
342 else:
358 handler(item)
343 handler(item)
359 return True
344 return True
360
345
361 def _show_interpreter_prompt(self, number=None):
346 def _show_interpreter_prompt(self, number=None):
362 """ Reimplemented for IPython-style prompts.
347 """ Reimplemented for IPython-style prompts.
363 """
348 """
364 # If a number was not specified, make a prompt number request.
349 # If a number was not specified, make a prompt number request.
365 if number is None:
350 if number is None:
366 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
351 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
367 info = self._ExecutionRequest(msg_id, 'prompt')
352 info = self._ExecutionRequest(msg_id, 'prompt')
368 self._request_info['execute'][msg_id] = info
353 self._request_info['execute'][msg_id] = info
369 return
354 return
370
355
371 # Show a new prompt and save information about it so that it can be
356 # Show a new prompt and save information about it so that it can be
372 # updated later if the prompt number turns out to be wrong.
357 # updated later if the prompt number turns out to be wrong.
373 self._prompt_sep = self.input_sep
358 self._prompt_sep = self.input_sep
374 self._show_prompt(self._make_in_prompt(number), html=True)
359 self._show_prompt(self._make_in_prompt(number), html=True)
375 block = self._control.document().lastBlock()
360 block = self._control.document().lastBlock()
376 length = len(self._prompt)
361 length = len(self._prompt)
377 self._previous_prompt_obj = self._PromptBlock(block, length, number)
362 self._previous_prompt_obj = self._PromptBlock(block, length, number)
378
363
379 # Update continuation prompt to reflect (possibly) new prompt length.
364 # Update continuation prompt to reflect (possibly) new prompt length.
380 self._set_continuation_prompt(
365 self._set_continuation_prompt(
381 self._make_continuation_prompt(self._prompt), html=True)
366 self._make_continuation_prompt(self._prompt), html=True)
382
367
383 def _show_interpreter_prompt_for_reply(self, msg):
368 def _show_interpreter_prompt_for_reply(self, msg):
384 """ Reimplemented for IPython-style prompts.
369 """ Reimplemented for IPython-style prompts.
385 """
370 """
386 # Update the old prompt number if necessary.
371 # Update the old prompt number if necessary.
387 content = msg['content']
372 content = msg['content']
388 # abort replies do not have any keys:
373 # abort replies do not have any keys:
389 if content['status'] == 'aborted':
374 if content['status'] == 'aborted':
390 if self._previous_prompt_obj:
375 if self._previous_prompt_obj:
391 previous_prompt_number = self._previous_prompt_obj.number
376 previous_prompt_number = self._previous_prompt_obj.number
392 else:
377 else:
393 previous_prompt_number = 0
378 previous_prompt_number = 0
394 else:
379 else:
395 previous_prompt_number = content['execution_count']
380 previous_prompt_number = content['execution_count']
396 if self._previous_prompt_obj and \
381 if self._previous_prompt_obj and \
397 self._previous_prompt_obj.number != previous_prompt_number:
382 self._previous_prompt_obj.number != previous_prompt_number:
398 block = self._previous_prompt_obj.block
383 block = self._previous_prompt_obj.block
399
384
400 # Make sure the prompt block has not been erased.
385 # Make sure the prompt block has not been erased.
401 if block.isValid() and block.text():
386 if block.isValid() and block.text():
402
387
403 # Remove the old prompt and insert a new prompt.
388 # Remove the old prompt and insert a new prompt.
404 cursor = QtGui.QTextCursor(block)
389 cursor = QtGui.QTextCursor(block)
405 cursor.movePosition(QtGui.QTextCursor.Right,
390 cursor.movePosition(QtGui.QTextCursor.Right,
406 QtGui.QTextCursor.KeepAnchor,
391 QtGui.QTextCursor.KeepAnchor,
407 self._previous_prompt_obj.length)
392 self._previous_prompt_obj.length)
408 prompt = self._make_in_prompt(previous_prompt_number)
393 prompt = self._make_in_prompt(previous_prompt_number)
409 self._prompt = self._insert_html_fetching_plain_text(
394 self._prompt = self._insert_html_fetching_plain_text(
410 cursor, prompt)
395 cursor, prompt)
411
396
412 # When the HTML is inserted, Qt blows away the syntax
397 # When the HTML is inserted, Qt blows away the syntax
413 # highlighting for the line, so we need to rehighlight it.
398 # highlighting for the line, so we need to rehighlight it.
414 self._highlighter.rehighlightBlock(cursor.block())
399 self._highlighter.rehighlightBlock(cursor.block())
415
400
416 self._previous_prompt_obj = None
401 self._previous_prompt_obj = None
417
402
418 # Show a new prompt with the kernel's estimated prompt number.
403 # Show a new prompt with the kernel's estimated prompt number.
419 self._show_interpreter_prompt(previous_prompt_number + 1)
404 self._show_interpreter_prompt(previous_prompt_number + 1)
420
405
421 #---------------------------------------------------------------------------
406 #---------------------------------------------------------------------------
422 # 'IPythonWidget' interface
407 # 'IPythonWidget' interface
423 #---------------------------------------------------------------------------
408 #---------------------------------------------------------------------------
424
409
425 def set_default_style(self, colors='lightbg'):
410 def set_default_style(self, colors='lightbg'):
426 """ Sets the widget style to the class defaults.
411 """ Sets the widget style to the class defaults.
427
412
428 Parameters:
413 Parameters:
429 -----------
414 -----------
430 colors : str, optional (default lightbg)
415 colors : str, optional (default lightbg)
431 Whether to use the default IPython light background or dark
416 Whether to use the default IPython light background or dark
432 background or B&W style.
417 background or B&W style.
433 """
418 """
434 colors = colors.lower()
419 colors = colors.lower()
435 if colors=='lightbg':
420 if colors=='lightbg':
436 self.style_sheet = styles.default_light_style_sheet
421 self.style_sheet = styles.default_light_style_sheet
437 self.syntax_style = styles.default_light_syntax_style
422 self.syntax_style = styles.default_light_syntax_style
438 elif colors=='linux':
423 elif colors=='linux':
439 self.style_sheet = styles.default_dark_style_sheet
424 self.style_sheet = styles.default_dark_style_sheet
440 self.syntax_style = styles.default_dark_syntax_style
425 self.syntax_style = styles.default_dark_syntax_style
441 elif colors=='nocolor':
426 elif colors=='nocolor':
442 self.style_sheet = styles.default_bw_style_sheet
427 self.style_sheet = styles.default_bw_style_sheet
443 self.syntax_style = styles.default_bw_syntax_style
428 self.syntax_style = styles.default_bw_syntax_style
444 else:
429 else:
445 raise KeyError("No such color scheme: %s"%colors)
430 raise KeyError("No such color scheme: %s"%colors)
446
431
447 #---------------------------------------------------------------------------
432 #---------------------------------------------------------------------------
448 # 'IPythonWidget' protected interface
433 # 'IPythonWidget' protected interface
449 #---------------------------------------------------------------------------
434 #---------------------------------------------------------------------------
450
435
451 def _edit(self, filename, line=None):
436 def _edit(self, filename, line=None):
452 """ Opens a Python script for editing.
437 """ Opens a Python script for editing.
453
438
454 Parameters:
439 Parameters:
455 -----------
440 -----------
456 filename : str
441 filename : str
457 A path to a local system file.
442 A path to a local system file.
458
443
459 line : int, optional
444 line : int, optional
460 A line of interest in the file.
445 A line of interest in the file.
461 """
446 """
462 if self.custom_edit:
447 if self.custom_edit:
463 self.custom_edit_requested.emit(filename, line)
448 self.custom_edit_requested.emit(filename, line)
464 elif not self.editor:
449 elif not self.editor:
465 self._append_plain_text('No default editor available.\n'
450 self._append_plain_text('No default editor available.\n'
466 'Specify a GUI text editor in the `IPythonWidget.editor` '
451 'Specify a GUI text editor in the `IPythonWidget.editor` '
467 'configurable to enable the %edit magic')
452 'configurable to enable the %edit magic')
468 else:
453 else:
469 try:
454 try:
470 filename = '"%s"' % filename
455 filename = '"%s"' % filename
471 if line and self.editor_line:
456 if line and self.editor_line:
472 command = self.editor_line.format(filename=filename,
457 command = self.editor_line.format(filename=filename,
473 line=line)
458 line=line)
474 else:
459 else:
475 try:
460 try:
476 command = self.editor.format()
461 command = self.editor.format()
477 except KeyError:
462 except KeyError:
478 command = self.editor.format(filename=filename)
463 command = self.editor.format(filename=filename)
479 else:
464 else:
480 command += ' ' + filename
465 command += ' ' + filename
481 except KeyError:
466 except KeyError:
482 self._append_plain_text('Invalid editor command.\n')
467 self._append_plain_text('Invalid editor command.\n')
483 else:
468 else:
484 try:
469 try:
485 Popen(command, shell=True)
470 Popen(command, shell=True)
486 except OSError:
471 except OSError:
487 msg = 'Opening editor with command "%s" failed.\n'
472 msg = 'Opening editor with command "%s" failed.\n'
488 self._append_plain_text(msg % command)
473 self._append_plain_text(msg % command)
489
474
490 def _make_in_prompt(self, number):
475 def _make_in_prompt(self, number):
491 """ Given a prompt number, returns an HTML In prompt.
476 """ Given a prompt number, returns an HTML In prompt.
492 """
477 """
493 try:
478 try:
494 body = self.in_prompt % number
479 body = self.in_prompt % number
495 except TypeError:
480 except TypeError:
496 # allow in_prompt to leave out number, e.g. '>>> '
481 # allow in_prompt to leave out number, e.g. '>>> '
497 body = self.in_prompt
482 body = self.in_prompt
498 return '<span class="in-prompt">%s</span>' % body
483 return '<span class="in-prompt">%s</span>' % body
499
484
500 def _make_continuation_prompt(self, prompt):
485 def _make_continuation_prompt(self, prompt):
501 """ Given a plain text version of an In prompt, returns an HTML
486 """ Given a plain text version of an In prompt, returns an HTML
502 continuation prompt.
487 continuation prompt.
503 """
488 """
504 end_chars = '...: '
489 end_chars = '...: '
505 space_count = len(prompt.lstrip('\n')) - len(end_chars)
490 space_count = len(prompt.lstrip('\n')) - len(end_chars)
506 body = '&nbsp;' * space_count + end_chars
491 body = '&nbsp;' * space_count + end_chars
507 return '<span class="in-prompt">%s</span>' % body
492 return '<span class="in-prompt">%s</span>' % body
508
493
509 def _make_out_prompt(self, number):
494 def _make_out_prompt(self, number):
510 """ Given a prompt number, returns an HTML Out prompt.
495 """ Given a prompt number, returns an HTML Out prompt.
511 """
496 """
512 body = self.out_prompt % number
497 body = self.out_prompt % number
513 return '<span class="out-prompt">%s</span>' % body
498 return '<span class="out-prompt">%s</span>' % body
514
499
515 #------ Payload handlers --------------------------------------------------
500 #------ Payload handlers --------------------------------------------------
516
501
517 # Payload handlers with a generic interface: each takes the opaque payload
502 # Payload handlers with a generic interface: each takes the opaque payload
518 # dict, unpacks it and calls the underlying functions with the necessary
503 # dict, unpacks it and calls the underlying functions with the necessary
519 # arguments.
504 # arguments.
520
505
521 def _handle_payload_edit(self, item):
506 def _handle_payload_edit(self, item):
522 self._edit(item['filename'], item['line_number'])
507 self._edit(item['filename'], item['line_number'])
523
508
524 def _handle_payload_exit(self, item):
509 def _handle_payload_exit(self, item):
525 self._keep_kernel_on_exit = item['keepkernel']
510 self._keep_kernel_on_exit = item['keepkernel']
526 self.exit_requested.emit(self)
511 self.exit_requested.emit(self)
527
512
528 def _handle_payload_next_input(self, item):
513 def _handle_payload_next_input(self, item):
529 self.input_buffer = dedent(item['text'].rstrip())
514 self.input_buffer = dedent(item['text'].rstrip())
530
515
531 def _handle_payload_page(self, item):
516 def _handle_payload_page(self, item):
532 # Since the plain text widget supports only a very small subset of HTML
517 # Since the plain text widget supports only a very small subset of HTML
533 # and we have no control over the HTML source, we only page HTML
518 # and we have no control over the HTML source, we only page HTML
534 # payloads in the rich text widget.
519 # payloads in the rich text widget.
535 if item['html'] and self.kind == 'rich':
520 if item['html'] and self.kind == 'rich':
536 self._page(item['html'], html=True)
521 self._page(item['html'], html=True)
537 else:
522 else:
538 self._page(item['text'], html=False)
523 self._page(item['text'], html=False)
539
524
540 #------ Trait change handlers --------------------------------------------
525 #------ Trait change handlers --------------------------------------------
541
526
542 def _style_sheet_changed(self):
527 def _style_sheet_changed(self):
543 """ Set the style sheets of the underlying widgets.
528 """ Set the style sheets of the underlying widgets.
544 """
529 """
545 self.setStyleSheet(self.style_sheet)
530 self.setStyleSheet(self.style_sheet)
546 self._control.document().setDefaultStyleSheet(self.style_sheet)
531 self._control.document().setDefaultStyleSheet(self.style_sheet)
547 if self._page_control:
532 if self._page_control:
548 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
533 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
549
534
550 bg_color = self._control.palette().window().color()
535 bg_color = self._control.palette().window().color()
551 self._ansi_processor.set_background_color(bg_color)
536 self._ansi_processor.set_background_color(bg_color)
552
537
553
538
554 def _syntax_style_changed(self):
539 def _syntax_style_changed(self):
555 """ Set the style for the syntax highlighter.
540 """ Set the style for the syntax highlighter.
556 """
541 """
557 if self._highlighter is None:
542 if self._highlighter is None:
558 # ignore premature calls
543 # ignore premature calls
559 return
544 return
560 if self.syntax_style:
545 if self.syntax_style:
561 self._highlighter.set_style(self.syntax_style)
546 self._highlighter.set_style(self.syntax_style)
562 else:
547 else:
563 self._highlighter.set_style_sheet(self.style_sheet)
548 self._highlighter.set_style_sheet(self.style_sheet)
564
549
565 #------ Trait default initializers -----------------------------------------
550 #------ Trait default initializers -----------------------------------------
566
551
567 def _banner_default(self):
552 def _banner_default(self):
568 from IPython.core.usage import default_gui_banner
553 from IPython.core.usage import default_gui_banner
569 return default_gui_banner
554 return default_gui_banner
General Comments 0
You need to be logged in to leave comments. Login now