##// END OF EJS Templates
Merge pull request #4800 from pankajp/handle-aborted-prompt-reply...
Min RK -
r14210:d0865cfa merge
parent child Browse files
Show More
@@ -1,598 +1,602 b''
1 """A FrontendWidget that emulates the interface of the console IPython.
1 """A FrontendWidget that emulates the interface of the console IPython.
2
2
3 This supports the additional functionality provided by the IPython kernel.
3 This supports the additional functionality provided by the IPython kernel.
4 """
4 """
5
5
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Imports
7 # Imports
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9
9
10 # Standard library imports
10 # Standard library imports
11 from collections import namedtuple
11 from collections import namedtuple
12 import os.path
12 import os.path
13 import re
13 import re
14 from subprocess import Popen
14 from subprocess import Popen
15 import sys
15 import sys
16 import time
16 import time
17 from textwrap import dedent
17 from textwrap import dedent
18
18
19 # System library imports
19 # System library imports
20 from IPython.external.qt import QtCore, QtGui
20 from IPython.external.qt import QtCore, QtGui
21
21
22 # Local imports
22 # Local imports
23 from IPython.core.inputsplitter import IPythonInputSplitter
23 from IPython.core.inputsplitter import IPythonInputSplitter
24 from IPython.core.inputtransformer import ipy_prompt
24 from IPython.core.inputtransformer import ipy_prompt
25 from IPython.utils.traitlets import Bool, Unicode
25 from IPython.utils.traitlets import Bool, Unicode
26 from .frontend_widget import FrontendWidget
26 from .frontend_widget import FrontendWidget
27 from . import styles
27 from . import styles
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Constants
30 # Constants
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33 # Default strings to build and display input and output prompts (and separators
33 # Default strings to build and display input and output prompts (and separators
34 # in between)
34 # in between)
35 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
36 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
37 default_input_sep = '\n'
37 default_input_sep = '\n'
38 default_output_sep = ''
38 default_output_sep = ''
39 default_output_sep2 = ''
39 default_output_sep2 = ''
40
40
41 # Base path for most payload sources.
41 # Base path for most payload sources.
42 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
42 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
43
43
44 if sys.platform.startswith('win'):
44 if sys.platform.startswith('win'):
45 default_editor = 'notepad'
45 default_editor = 'notepad'
46 else:
46 else:
47 default_editor = ''
47 default_editor = ''
48
48
49 #-----------------------------------------------------------------------------
49 #-----------------------------------------------------------------------------
50 # IPythonWidget class
50 # IPythonWidget class
51 #-----------------------------------------------------------------------------
51 #-----------------------------------------------------------------------------
52
52
53 class IPythonWidget(FrontendWidget):
53 class IPythonWidget(FrontendWidget):
54 """ A FrontendWidget for an IPython kernel.
54 """ A FrontendWidget for an IPython kernel.
55 """
55 """
56
56
57 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
58 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
59 # settings.
59 # settings.
60 custom_edit = Bool(False)
60 custom_edit = Bool(False)
61 custom_edit_requested = QtCore.Signal(object, object)
61 custom_edit_requested = QtCore.Signal(object, object)
62
62
63 editor = Unicode(default_editor, config=True,
63 editor = Unicode(default_editor, config=True,
64 help="""
64 help="""
65 A command for invoking a system text editor. If the string contains a
65 A command for invoking a system text editor. If the string contains a
66 {filename} format specifier, it will be used. Otherwise, the filename
66 {filename} format specifier, it will be used. Otherwise, the filename
67 will be appended to the end the command.
67 will be appended to the end the command.
68 """)
68 """)
69
69
70 editor_line = Unicode(config=True,
70 editor_line = Unicode(config=True,
71 help="""
71 help="""
72 The editor command to use when a specific line number is requested. The
72 The editor command to use when a specific line number is requested. The
73 string should contain two format specifiers: {line} and {filename}. If
73 string should contain two format specifiers: {line} and {filename}. If
74 this parameter is not specified, the line number option to the %edit
74 this parameter is not specified, the line number option to the %edit
75 magic will be ignored.
75 magic will be ignored.
76 """)
76 """)
77
77
78 style_sheet = Unicode(config=True,
78 style_sheet = Unicode(config=True,
79 help="""
79 help="""
80 A CSS stylesheet. The stylesheet can contain classes for:
80 A CSS stylesheet. The stylesheet can contain classes for:
81 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
82 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
83 3. IPython: .error, .in-prompt, .out-prompt, etc
83 3. IPython: .error, .in-prompt, .out-prompt, etc
84 """)
84 """)
85
85
86 syntax_style = Unicode(config=True,
86 syntax_style = Unicode(config=True,
87 help="""
87 help="""
88 If not empty, use this Pygments style for syntax highlighting.
88 If not empty, use this Pygments style for syntax highlighting.
89 Otherwise, the style sheet is queried for Pygments style
89 Otherwise, the style sheet is queried for Pygments style
90 information.
90 information.
91 """)
91 """)
92
92
93 # Prompts.
93 # Prompts.
94 in_prompt = Unicode(default_in_prompt, config=True)
94 in_prompt = Unicode(default_in_prompt, config=True)
95 out_prompt = Unicode(default_out_prompt, config=True)
95 out_prompt = Unicode(default_out_prompt, config=True)
96 input_sep = Unicode(default_input_sep, config=True)
96 input_sep = Unicode(default_input_sep, config=True)
97 output_sep = Unicode(default_output_sep, config=True)
97 output_sep = Unicode(default_output_sep, config=True)
98 output_sep2 = Unicode(default_output_sep2, config=True)
98 output_sep2 = Unicode(default_output_sep2, config=True)
99
99
100 # FrontendWidget protected class variables.
100 # FrontendWidget protected class variables.
101 _input_splitter_class = IPythonInputSplitter
101 _input_splitter_class = IPythonInputSplitter
102 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
102 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
103 logical_line_transforms=[],
103 logical_line_transforms=[],
104 python_line_transforms=[],
104 python_line_transforms=[],
105 )
105 )
106
106
107 # IPythonWidget protected class variables.
107 # IPythonWidget protected class variables.
108 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
108 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
109 _payload_source_edit = 'edit_magic'
109 _payload_source_edit = 'edit_magic'
110 _payload_source_exit = 'ask_exit'
110 _payload_source_exit = 'ask_exit'
111 _payload_source_next_input = 'set_next_input'
111 _payload_source_next_input = 'set_next_input'
112 _payload_source_page = 'page'
112 _payload_source_page = 'page'
113 _retrying_history_request = False
113 _retrying_history_request = False
114
114
115 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
116 # 'object' interface
116 # 'object' interface
117 #---------------------------------------------------------------------------
117 #---------------------------------------------------------------------------
118
118
119 def __init__(self, *args, **kw):
119 def __init__(self, *args, **kw):
120 super(IPythonWidget, self).__init__(*args, **kw)
120 super(IPythonWidget, self).__init__(*args, **kw)
121
121
122 # IPythonWidget protected variables.
122 # IPythonWidget protected variables.
123 self._payload_handlers = {
123 self._payload_handlers = {
124 self._payload_source_edit : self._handle_payload_edit,
124 self._payload_source_edit : self._handle_payload_edit,
125 self._payload_source_exit : self._handle_payload_exit,
125 self._payload_source_exit : self._handle_payload_exit,
126 self._payload_source_page : self._handle_payload_page,
126 self._payload_source_page : self._handle_payload_page,
127 self._payload_source_next_input : self._handle_payload_next_input }
127 self._payload_source_next_input : self._handle_payload_next_input }
128 self._previous_prompt_obj = None
128 self._previous_prompt_obj = None
129 self._keep_kernel_on_exit = None
129 self._keep_kernel_on_exit = None
130
130
131 # Initialize widget styling.
131 # Initialize widget styling.
132 if self.style_sheet:
132 if self.style_sheet:
133 self._style_sheet_changed()
133 self._style_sheet_changed()
134 self._syntax_style_changed()
134 self._syntax_style_changed()
135 else:
135 else:
136 self.set_default_style()
136 self.set_default_style()
137
137
138 self._guiref_loaded = False
138 self._guiref_loaded = False
139
139
140 #---------------------------------------------------------------------------
140 #---------------------------------------------------------------------------
141 # 'BaseFrontendMixin' abstract interface
141 # 'BaseFrontendMixin' abstract interface
142 #---------------------------------------------------------------------------
142 #---------------------------------------------------------------------------
143
143
144 def _handle_complete_reply(self, rep):
144 def _handle_complete_reply(self, rep):
145 """ Reimplemented to support IPython's improved completion machinery.
145 """ Reimplemented to support IPython's improved completion machinery.
146 """
146 """
147 self.log.debug("complete: %s", rep.get('content', ''))
147 self.log.debug("complete: %s", rep.get('content', ''))
148 cursor = self._get_cursor()
148 cursor = self._get_cursor()
149 info = self._request_info.get('complete')
149 info = self._request_info.get('complete')
150 if info and info.id == rep['parent_header']['msg_id'] and \
150 if info and info.id == rep['parent_header']['msg_id'] and \
151 info.pos == cursor.position():
151 info.pos == cursor.position():
152 matches = rep['content']['matches']
152 matches = rep['content']['matches']
153 text = rep['content']['matched_text']
153 text = rep['content']['matched_text']
154 offset = len(text)
154 offset = len(text)
155
155
156 # Clean up matches with period and path separators if the matched
156 # Clean up matches with period and path separators if the matched
157 # text has not been transformed. This is done by truncating all
157 # text has not been transformed. This is done by truncating all
158 # but the last component and then suitably decreasing the offset
158 # but the last component and then suitably decreasing the offset
159 # between the current cursor position and the start of completion.
159 # between the current cursor position and the start of completion.
160 if len(matches) > 1 and matches[0][:offset] == text:
160 if len(matches) > 1 and matches[0][:offset] == text:
161 parts = re.split(r'[./\\]', text)
161 parts = re.split(r'[./\\]', text)
162 sep_count = len(parts) - 1
162 sep_count = len(parts) - 1
163 if sep_count:
163 if sep_count:
164 chop_length = sum(map(len, parts[:sep_count])) + sep_count
164 chop_length = sum(map(len, parts[:sep_count])) + sep_count
165 matches = [ match[chop_length:] for match in matches ]
165 matches = [ match[chop_length:] for match in matches ]
166 offset -= chop_length
166 offset -= chop_length
167
167
168 # Move the cursor to the start of the match and complete.
168 # Move the cursor to the start of the match and complete.
169 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
169 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
170 self._complete_with_items(cursor, matches)
170 self._complete_with_items(cursor, matches)
171
171
172 def _handle_execute_reply(self, msg):
172 def _handle_execute_reply(self, msg):
173 """ Reimplemented to support prompt requests.
173 """ Reimplemented to support prompt requests.
174 """
174 """
175 msg_id = msg['parent_header'].get('msg_id')
175 msg_id = msg['parent_header'].get('msg_id')
176 info = self._request_info['execute'].get(msg_id)
176 info = self._request_info['execute'].get(msg_id)
177 if info and info.kind == 'prompt':
177 if info and info.kind == 'prompt':
178 number = msg['content']['execution_count'] + 1
178 content = msg['content']
179 self._show_interpreter_prompt(number)
179 if content['status'] == 'aborted':
180 self._request_info['execute'].pop(msg_id)
180 self._show_interpreter_prompt()
181 else:
182 number = content['execution_count'] + 1
183 self._show_interpreter_prompt(number)
184 self._request_info['execute'].pop(msg_id)
181 else:
185 else:
182 super(IPythonWidget, self)._handle_execute_reply(msg)
186 super(IPythonWidget, self)._handle_execute_reply(msg)
183
187
184 def _handle_history_reply(self, msg):
188 def _handle_history_reply(self, msg):
185 """ Implemented to handle history tail replies, which are only supported
189 """ Implemented to handle history tail replies, which are only supported
186 by the IPython kernel.
190 by the IPython kernel.
187 """
191 """
188 content = msg['content']
192 content = msg['content']
189 if 'history' not in content:
193 if 'history' not in content:
190 self.log.error("History request failed: %r"%content)
194 self.log.error("History request failed: %r"%content)
191 if content.get('status', '') == 'aborted' and \
195 if content.get('status', '') == 'aborted' and \
192 not self._retrying_history_request:
196 not self._retrying_history_request:
193 # a *different* action caused this request to be aborted, so
197 # a *different* action caused this request to be aborted, so
194 # we should try again.
198 # we should try again.
195 self.log.error("Retrying aborted history request")
199 self.log.error("Retrying aborted history request")
196 # prevent multiple retries of aborted requests:
200 # prevent multiple retries of aborted requests:
197 self._retrying_history_request = True
201 self._retrying_history_request = True
198 # wait out the kernel's queue flush, which is currently timed at 0.1s
202 # wait out the kernel's queue flush, which is currently timed at 0.1s
199 time.sleep(0.25)
203 time.sleep(0.25)
200 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
204 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
201 else:
205 else:
202 self._retrying_history_request = False
206 self._retrying_history_request = False
203 return
207 return
204 # reset retry flag
208 # reset retry flag
205 self._retrying_history_request = False
209 self._retrying_history_request = False
206 history_items = content['history']
210 history_items = content['history']
207 self.log.debug("Received history reply with %i entries", len(history_items))
211 self.log.debug("Received history reply with %i entries", len(history_items))
208 items = []
212 items = []
209 last_cell = u""
213 last_cell = u""
210 for _, _, cell in history_items:
214 for _, _, cell in history_items:
211 cell = cell.rstrip()
215 cell = cell.rstrip()
212 if cell != last_cell:
216 if cell != last_cell:
213 items.append(cell)
217 items.append(cell)
214 last_cell = cell
218 last_cell = cell
215 self._set_history(items)
219 self._set_history(items)
216
220
217 def _handle_pyout(self, msg):
221 def _handle_pyout(self, msg):
218 """ Reimplemented for IPython-style "display hook".
222 """ Reimplemented for IPython-style "display hook".
219 """
223 """
220 self.log.debug("pyout: %s", msg.get('content', ''))
224 self.log.debug("pyout: %s", msg.get('content', ''))
221 if not self._hidden and self._is_from_this_session(msg):
225 if not self._hidden and self._is_from_this_session(msg):
222 content = msg['content']
226 content = msg['content']
223 prompt_number = content.get('execution_count', 0)
227 prompt_number = content.get('execution_count', 0)
224 data = content['data']
228 data = content['data']
225 if 'text/html' in data:
229 if 'text/html' in data:
226 self._append_plain_text(self.output_sep, True)
230 self._append_plain_text(self.output_sep, True)
227 self._append_html(self._make_out_prompt(prompt_number), True)
231 self._append_html(self._make_out_prompt(prompt_number), True)
228 html = data['text/html']
232 html = data['text/html']
229 self._append_plain_text('\n', True)
233 self._append_plain_text('\n', True)
230 self._append_html(html + self.output_sep2, True)
234 self._append_html(html + self.output_sep2, True)
231 elif 'text/plain' in data:
235 elif 'text/plain' in data:
232 self._append_plain_text(self.output_sep, True)
236 self._append_plain_text(self.output_sep, True)
233 self._append_html(self._make_out_prompt(prompt_number), True)
237 self._append_html(self._make_out_prompt(prompt_number), True)
234 text = data['text/plain']
238 text = data['text/plain']
235 # If the repr is multiline, make sure we start on a new line,
239 # If the repr is multiline, make sure we start on a new line,
236 # so that its lines are aligned.
240 # so that its lines are aligned.
237 if "\n" in text and not self.output_sep.endswith("\n"):
241 if "\n" in text and not self.output_sep.endswith("\n"):
238 self._append_plain_text('\n', True)
242 self._append_plain_text('\n', True)
239 self._append_plain_text(text + self.output_sep2, True)
243 self._append_plain_text(text + self.output_sep2, True)
240
244
241 def _handle_display_data(self, msg):
245 def _handle_display_data(self, msg):
242 """ The base handler for the ``display_data`` message.
246 """ The base handler for the ``display_data`` message.
243 """
247 """
244 self.log.debug("display: %s", msg.get('content', ''))
248 self.log.debug("display: %s", msg.get('content', ''))
245 # For now, we don't display data from other frontends, but we
249 # For now, we don't display data from other frontends, but we
246 # eventually will as this allows all frontends to monitor the display
250 # eventually will as this allows all frontends to monitor the display
247 # data. But we need to figure out how to handle this in the GUI.
251 # data. But we need to figure out how to handle this in the GUI.
248 if not self._hidden and self._is_from_this_session(msg):
252 if not self._hidden and self._is_from_this_session(msg):
249 source = msg['content']['source']
253 source = msg['content']['source']
250 data = msg['content']['data']
254 data = msg['content']['data']
251 metadata = msg['content']['metadata']
255 metadata = msg['content']['metadata']
252 # In the regular IPythonWidget, we simply print the plain text
256 # In the regular IPythonWidget, we simply print the plain text
253 # representation.
257 # representation.
254 if 'text/html' in data:
258 if 'text/html' in data:
255 html = data['text/html']
259 html = data['text/html']
256 self._append_html(html, True)
260 self._append_html(html, True)
257 elif 'text/plain' in data:
261 elif 'text/plain' in data:
258 text = data['text/plain']
262 text = data['text/plain']
259 self._append_plain_text(text, True)
263 self._append_plain_text(text, True)
260 # This newline seems to be needed for text and html output.
264 # This newline seems to be needed for text and html output.
261 self._append_plain_text(u'\n', True)
265 self._append_plain_text(u'\n', True)
262
266
263 def _handle_kernel_info_reply(self, rep):
267 def _handle_kernel_info_reply(self, rep):
264 """ Handle kernel info replies.
268 """ Handle kernel info replies.
265 """
269 """
266 if not self._guiref_loaded:
270 if not self._guiref_loaded:
267 if rep['content'].get('language') == 'python':
271 if rep['content'].get('language') == 'python':
268 self._load_guiref_magic()
272 self._load_guiref_magic()
269 self._guiref_loaded = True
273 self._guiref_loaded = True
270
274
271 def _started_channels(self):
275 def _started_channels(self):
272 """Reimplemented to make a history request and load %guiref."""
276 """Reimplemented to make a history request and load %guiref."""
273 super(IPythonWidget, self)._started_channels()
277 super(IPythonWidget, self)._started_channels()
274
278
275 # The reply will trigger %guiref load provided language=='python'
279 # The reply will trigger %guiref load provided language=='python'
276 self.kernel_client.kernel_info()
280 self.kernel_client.kernel_info()
277
281
278 self.kernel_client.shell_channel.history(hist_access_type='tail',
282 self.kernel_client.shell_channel.history(hist_access_type='tail',
279 n=1000)
283 n=1000)
280
284
281 def _started_kernel(self):
285 def _started_kernel(self):
282 """Load %guiref when the kernel starts (if channels are also started).
286 """Load %guiref when the kernel starts (if channels are also started).
283
287
284 Principally triggered by kernel restart.
288 Principally triggered by kernel restart.
285 """
289 """
286 if self.kernel_client.shell_channel is not None:
290 if self.kernel_client.shell_channel is not None:
287 self._load_guiref_magic()
291 self._load_guiref_magic()
288
292
289 def _load_guiref_magic(self):
293 def _load_guiref_magic(self):
290 """Load %guiref magic."""
294 """Load %guiref magic."""
291 self.kernel_client.shell_channel.execute('\n'.join([
295 self.kernel_client.shell_channel.execute('\n'.join([
292 "try:",
296 "try:",
293 " _usage",
297 " _usage",
294 "except:",
298 "except:",
295 " from IPython.core import usage as _usage",
299 " from IPython.core import usage as _usage",
296 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
300 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
297 " del _usage",
301 " del _usage",
298 ]), silent=True)
302 ]), silent=True)
299
303
300 #---------------------------------------------------------------------------
304 #---------------------------------------------------------------------------
301 # 'ConsoleWidget' public interface
305 # 'ConsoleWidget' public interface
302 #---------------------------------------------------------------------------
306 #---------------------------------------------------------------------------
303
307
304 #---------------------------------------------------------------------------
308 #---------------------------------------------------------------------------
305 # 'FrontendWidget' public interface
309 # 'FrontendWidget' public interface
306 #---------------------------------------------------------------------------
310 #---------------------------------------------------------------------------
307
311
308 def execute_file(self, path, hidden=False):
312 def execute_file(self, path, hidden=False):
309 """ Reimplemented to use the 'run' magic.
313 """ Reimplemented to use the 'run' magic.
310 """
314 """
311 # Use forward slashes on Windows to avoid escaping each separator.
315 # Use forward slashes on Windows to avoid escaping each separator.
312 if sys.platform == 'win32':
316 if sys.platform == 'win32':
313 path = os.path.normpath(path).replace('\\', '/')
317 path = os.path.normpath(path).replace('\\', '/')
314
318
315 # Perhaps we should not be using %run directly, but while we
319 # Perhaps we should not be using %run directly, but while we
316 # are, it is necessary to quote or escape filenames containing spaces
320 # are, it is necessary to quote or escape filenames containing spaces
317 # or quotes.
321 # or quotes.
318
322
319 # In earlier code here, to minimize escaping, we sometimes quoted the
323 # In earlier code here, to minimize escaping, we sometimes quoted the
320 # filename with single quotes. But to do this, this code must be
324 # filename with single quotes. But to do this, this code must be
321 # platform-aware, because run uses shlex rather than python string
325 # platform-aware, because run uses shlex rather than python string
322 # parsing, so that:
326 # parsing, so that:
323 # * In Win: single quotes can be used in the filename without quoting,
327 # * In Win: single quotes can be used in the filename without quoting,
324 # and we cannot use single quotes to quote the filename.
328 # and we cannot use single quotes to quote the filename.
325 # * In *nix: we can escape double quotes in a double quoted filename,
329 # * In *nix: we can escape double quotes in a double quoted filename,
326 # but can't escape single quotes in a single quoted filename.
330 # but can't escape single quotes in a single quoted filename.
327
331
328 # So to keep this code non-platform-specific and simple, we now only
332 # So to keep this code non-platform-specific and simple, we now only
329 # use double quotes to quote filenames, and escape when needed:
333 # use double quotes to quote filenames, and escape when needed:
330 if ' ' in path or "'" in path or '"' in path:
334 if ' ' in path or "'" in path or '"' in path:
331 path = '"%s"' % path.replace('"', '\\"')
335 path = '"%s"' % path.replace('"', '\\"')
332 self.execute('%%run %s' % path, hidden=hidden)
336 self.execute('%%run %s' % path, hidden=hidden)
333
337
334 #---------------------------------------------------------------------------
338 #---------------------------------------------------------------------------
335 # 'FrontendWidget' protected interface
339 # 'FrontendWidget' protected interface
336 #---------------------------------------------------------------------------
340 #---------------------------------------------------------------------------
337
341
338 def _complete(self):
342 def _complete(self):
339 """ Reimplemented to support IPython's improved completion machinery.
343 """ Reimplemented to support IPython's improved completion machinery.
340 """
344 """
341 # We let the kernel split the input line, so we *always* send an empty
345 # We let the kernel split the input line, so we *always* send an empty
342 # text field. Readline-based frontends do get a real text field which
346 # text field. Readline-based frontends do get a real text field which
343 # they can use.
347 # they can use.
344 text = ''
348 text = ''
345
349
346 # Send the completion request to the kernel
350 # Send the completion request to the kernel
347 msg_id = self.kernel_client.shell_channel.complete(
351 msg_id = self.kernel_client.shell_channel.complete(
348 text, # text
352 text, # text
349 self._get_input_buffer_cursor_line(), # line
353 self._get_input_buffer_cursor_line(), # line
350 self._get_input_buffer_cursor_column(), # cursor_pos
354 self._get_input_buffer_cursor_column(), # cursor_pos
351 self.input_buffer) # block
355 self.input_buffer) # block
352 pos = self._get_cursor().position()
356 pos = self._get_cursor().position()
353 info = self._CompletionRequest(msg_id, pos)
357 info = self._CompletionRequest(msg_id, pos)
354 self._request_info['complete'] = info
358 self._request_info['complete'] = info
355
359
356 def _process_execute_error(self, msg):
360 def _process_execute_error(self, msg):
357 """ Reimplemented for IPython-style traceback formatting.
361 """ Reimplemented for IPython-style traceback formatting.
358 """
362 """
359 content = msg['content']
363 content = msg['content']
360 traceback = '\n'.join(content['traceback']) + '\n'
364 traceback = '\n'.join(content['traceback']) + '\n'
361 if False:
365 if False:
362 # FIXME: For now, tracebacks come as plain text, so we can't use
366 # FIXME: For now, tracebacks come as plain text, so we can't use
363 # the html renderer yet. Once we refactor ultratb to produce
367 # the html renderer yet. Once we refactor ultratb to produce
364 # properly styled tracebacks, this branch should be the default
368 # properly styled tracebacks, this branch should be the default
365 traceback = traceback.replace(' ', '&nbsp;')
369 traceback = traceback.replace(' ', '&nbsp;')
366 traceback = traceback.replace('\n', '<br/>')
370 traceback = traceback.replace('\n', '<br/>')
367
371
368 ename = content['ename']
372 ename = content['ename']
369 ename_styled = '<span class="error">%s</span>' % ename
373 ename_styled = '<span class="error">%s</span>' % ename
370 traceback = traceback.replace(ename, ename_styled)
374 traceback = traceback.replace(ename, ename_styled)
371
375
372 self._append_html(traceback)
376 self._append_html(traceback)
373 else:
377 else:
374 # This is the fallback for now, using plain text with ansi escapes
378 # This is the fallback for now, using plain text with ansi escapes
375 self._append_plain_text(traceback)
379 self._append_plain_text(traceback)
376
380
377 def _process_execute_payload(self, item):
381 def _process_execute_payload(self, item):
378 """ Reimplemented to dispatch payloads to handler methods.
382 """ Reimplemented to dispatch payloads to handler methods.
379 """
383 """
380 handler = self._payload_handlers.get(item['source'])
384 handler = self._payload_handlers.get(item['source'])
381 if handler is None:
385 if handler is None:
382 # We have no handler for this type of payload, simply ignore it
386 # We have no handler for this type of payload, simply ignore it
383 return False
387 return False
384 else:
388 else:
385 handler(item)
389 handler(item)
386 return True
390 return True
387
391
388 def _show_interpreter_prompt(self, number=None):
392 def _show_interpreter_prompt(self, number=None):
389 """ Reimplemented for IPython-style prompts.
393 """ Reimplemented for IPython-style prompts.
390 """
394 """
391 # If a number was not specified, make a prompt number request.
395 # If a number was not specified, make a prompt number request.
392 if number is None:
396 if number is None:
393 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
397 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
394 info = self._ExecutionRequest(msg_id, 'prompt')
398 info = self._ExecutionRequest(msg_id, 'prompt')
395 self._request_info['execute'][msg_id] = info
399 self._request_info['execute'][msg_id] = info
396 return
400 return
397
401
398 # Show a new prompt and save information about it so that it can be
402 # Show a new prompt and save information about it so that it can be
399 # updated later if the prompt number turns out to be wrong.
403 # updated later if the prompt number turns out to be wrong.
400 self._prompt_sep = self.input_sep
404 self._prompt_sep = self.input_sep
401 self._show_prompt(self._make_in_prompt(number), html=True)
405 self._show_prompt(self._make_in_prompt(number), html=True)
402 block = self._control.document().lastBlock()
406 block = self._control.document().lastBlock()
403 length = len(self._prompt)
407 length = len(self._prompt)
404 self._previous_prompt_obj = self._PromptBlock(block, length, number)
408 self._previous_prompt_obj = self._PromptBlock(block, length, number)
405
409
406 # Update continuation prompt to reflect (possibly) new prompt length.
410 # Update continuation prompt to reflect (possibly) new prompt length.
407 self._set_continuation_prompt(
411 self._set_continuation_prompt(
408 self._make_continuation_prompt(self._prompt), html=True)
412 self._make_continuation_prompt(self._prompt), html=True)
409
413
410 def _show_interpreter_prompt_for_reply(self, msg):
414 def _show_interpreter_prompt_for_reply(self, msg):
411 """ Reimplemented for IPython-style prompts.
415 """ Reimplemented for IPython-style prompts.
412 """
416 """
413 # Update the old prompt number if necessary.
417 # Update the old prompt number if necessary.
414 content = msg['content']
418 content = msg['content']
415 # abort replies do not have any keys:
419 # abort replies do not have any keys:
416 if content['status'] == 'aborted':
420 if content['status'] == 'aborted':
417 if self._previous_prompt_obj:
421 if self._previous_prompt_obj:
418 previous_prompt_number = self._previous_prompt_obj.number
422 previous_prompt_number = self._previous_prompt_obj.number
419 else:
423 else:
420 previous_prompt_number = 0
424 previous_prompt_number = 0
421 else:
425 else:
422 previous_prompt_number = content['execution_count']
426 previous_prompt_number = content['execution_count']
423 if self._previous_prompt_obj and \
427 if self._previous_prompt_obj and \
424 self._previous_prompt_obj.number != previous_prompt_number:
428 self._previous_prompt_obj.number != previous_prompt_number:
425 block = self._previous_prompt_obj.block
429 block = self._previous_prompt_obj.block
426
430
427 # Make sure the prompt block has not been erased.
431 # Make sure the prompt block has not been erased.
428 if block.isValid() and block.text():
432 if block.isValid() and block.text():
429
433
430 # Remove the old prompt and insert a new prompt.
434 # Remove the old prompt and insert a new prompt.
431 cursor = QtGui.QTextCursor(block)
435 cursor = QtGui.QTextCursor(block)
432 cursor.movePosition(QtGui.QTextCursor.Right,
436 cursor.movePosition(QtGui.QTextCursor.Right,
433 QtGui.QTextCursor.KeepAnchor,
437 QtGui.QTextCursor.KeepAnchor,
434 self._previous_prompt_obj.length)
438 self._previous_prompt_obj.length)
435 prompt = self._make_in_prompt(previous_prompt_number)
439 prompt = self._make_in_prompt(previous_prompt_number)
436 self._prompt = self._insert_html_fetching_plain_text(
440 self._prompt = self._insert_html_fetching_plain_text(
437 cursor, prompt)
441 cursor, prompt)
438
442
439 # When the HTML is inserted, Qt blows away the syntax
443 # When the HTML is inserted, Qt blows away the syntax
440 # highlighting for the line, so we need to rehighlight it.
444 # highlighting for the line, so we need to rehighlight it.
441 self._highlighter.rehighlightBlock(cursor.block())
445 self._highlighter.rehighlightBlock(cursor.block())
442
446
443 self._previous_prompt_obj = None
447 self._previous_prompt_obj = None
444
448
445 # Show a new prompt with the kernel's estimated prompt number.
449 # Show a new prompt with the kernel's estimated prompt number.
446 self._show_interpreter_prompt(previous_prompt_number + 1)
450 self._show_interpreter_prompt(previous_prompt_number + 1)
447
451
448 #---------------------------------------------------------------------------
452 #---------------------------------------------------------------------------
449 # 'IPythonWidget' interface
453 # 'IPythonWidget' interface
450 #---------------------------------------------------------------------------
454 #---------------------------------------------------------------------------
451
455
452 def set_default_style(self, colors='lightbg'):
456 def set_default_style(self, colors='lightbg'):
453 """ Sets the widget style to the class defaults.
457 """ Sets the widget style to the class defaults.
454
458
455 Parameters
459 Parameters
456 ----------
460 ----------
457 colors : str, optional (default lightbg)
461 colors : str, optional (default lightbg)
458 Whether to use the default IPython light background or dark
462 Whether to use the default IPython light background or dark
459 background or B&W style.
463 background or B&W style.
460 """
464 """
461 colors = colors.lower()
465 colors = colors.lower()
462 if colors=='lightbg':
466 if colors=='lightbg':
463 self.style_sheet = styles.default_light_style_sheet
467 self.style_sheet = styles.default_light_style_sheet
464 self.syntax_style = styles.default_light_syntax_style
468 self.syntax_style = styles.default_light_syntax_style
465 elif colors=='linux':
469 elif colors=='linux':
466 self.style_sheet = styles.default_dark_style_sheet
470 self.style_sheet = styles.default_dark_style_sheet
467 self.syntax_style = styles.default_dark_syntax_style
471 self.syntax_style = styles.default_dark_syntax_style
468 elif colors=='nocolor':
472 elif colors=='nocolor':
469 self.style_sheet = styles.default_bw_style_sheet
473 self.style_sheet = styles.default_bw_style_sheet
470 self.syntax_style = styles.default_bw_syntax_style
474 self.syntax_style = styles.default_bw_syntax_style
471 else:
475 else:
472 raise KeyError("No such color scheme: %s"%colors)
476 raise KeyError("No such color scheme: %s"%colors)
473
477
474 #---------------------------------------------------------------------------
478 #---------------------------------------------------------------------------
475 # 'IPythonWidget' protected interface
479 # 'IPythonWidget' protected interface
476 #---------------------------------------------------------------------------
480 #---------------------------------------------------------------------------
477
481
478 def _edit(self, filename, line=None):
482 def _edit(self, filename, line=None):
479 """ Opens a Python script for editing.
483 """ Opens a Python script for editing.
480
484
481 Parameters
485 Parameters
482 ----------
486 ----------
483 filename : str
487 filename : str
484 A path to a local system file.
488 A path to a local system file.
485
489
486 line : int, optional
490 line : int, optional
487 A line of interest in the file.
491 A line of interest in the file.
488 """
492 """
489 if self.custom_edit:
493 if self.custom_edit:
490 self.custom_edit_requested.emit(filename, line)
494 self.custom_edit_requested.emit(filename, line)
491 elif not self.editor:
495 elif not self.editor:
492 self._append_plain_text('No default editor available.\n'
496 self._append_plain_text('No default editor available.\n'
493 'Specify a GUI text editor in the `IPythonWidget.editor` '
497 'Specify a GUI text editor in the `IPythonWidget.editor` '
494 'configurable to enable the %edit magic')
498 'configurable to enable the %edit magic')
495 else:
499 else:
496 try:
500 try:
497 filename = '"%s"' % filename
501 filename = '"%s"' % filename
498 if line and self.editor_line:
502 if line and self.editor_line:
499 command = self.editor_line.format(filename=filename,
503 command = self.editor_line.format(filename=filename,
500 line=line)
504 line=line)
501 else:
505 else:
502 try:
506 try:
503 command = self.editor.format()
507 command = self.editor.format()
504 except KeyError:
508 except KeyError:
505 command = self.editor.format(filename=filename)
509 command = self.editor.format(filename=filename)
506 else:
510 else:
507 command += ' ' + filename
511 command += ' ' + filename
508 except KeyError:
512 except KeyError:
509 self._append_plain_text('Invalid editor command.\n')
513 self._append_plain_text('Invalid editor command.\n')
510 else:
514 else:
511 try:
515 try:
512 Popen(command, shell=True)
516 Popen(command, shell=True)
513 except OSError:
517 except OSError:
514 msg = 'Opening editor with command "%s" failed.\n'
518 msg = 'Opening editor with command "%s" failed.\n'
515 self._append_plain_text(msg % command)
519 self._append_plain_text(msg % command)
516
520
517 def _make_in_prompt(self, number):
521 def _make_in_prompt(self, number):
518 """ Given a prompt number, returns an HTML In prompt.
522 """ Given a prompt number, returns an HTML In prompt.
519 """
523 """
520 try:
524 try:
521 body = self.in_prompt % number
525 body = self.in_prompt % number
522 except TypeError:
526 except TypeError:
523 # allow in_prompt to leave out number, e.g. '>>> '
527 # allow in_prompt to leave out number, e.g. '>>> '
524 body = self.in_prompt
528 body = self.in_prompt
525 return '<span class="in-prompt">%s</span>' % body
529 return '<span class="in-prompt">%s</span>' % body
526
530
527 def _make_continuation_prompt(self, prompt):
531 def _make_continuation_prompt(self, prompt):
528 """ Given a plain text version of an In prompt, returns an HTML
532 """ Given a plain text version of an In prompt, returns an HTML
529 continuation prompt.
533 continuation prompt.
530 """
534 """
531 end_chars = '...: '
535 end_chars = '...: '
532 space_count = len(prompt.lstrip('\n')) - len(end_chars)
536 space_count = len(prompt.lstrip('\n')) - len(end_chars)
533 body = '&nbsp;' * space_count + end_chars
537 body = '&nbsp;' * space_count + end_chars
534 return '<span class="in-prompt">%s</span>' % body
538 return '<span class="in-prompt">%s</span>' % body
535
539
536 def _make_out_prompt(self, number):
540 def _make_out_prompt(self, number):
537 """ Given a prompt number, returns an HTML Out prompt.
541 """ Given a prompt number, returns an HTML Out prompt.
538 """
542 """
539 body = self.out_prompt % number
543 body = self.out_prompt % number
540 return '<span class="out-prompt">%s</span>' % body
544 return '<span class="out-prompt">%s</span>' % body
541
545
542 #------ Payload handlers --------------------------------------------------
546 #------ Payload handlers --------------------------------------------------
543
547
544 # Payload handlers with a generic interface: each takes the opaque payload
548 # Payload handlers with a generic interface: each takes the opaque payload
545 # dict, unpacks it and calls the underlying functions with the necessary
549 # dict, unpacks it and calls the underlying functions with the necessary
546 # arguments.
550 # arguments.
547
551
548 def _handle_payload_edit(self, item):
552 def _handle_payload_edit(self, item):
549 self._edit(item['filename'], item['line_number'])
553 self._edit(item['filename'], item['line_number'])
550
554
551 def _handle_payload_exit(self, item):
555 def _handle_payload_exit(self, item):
552 self._keep_kernel_on_exit = item['keepkernel']
556 self._keep_kernel_on_exit = item['keepkernel']
553 self.exit_requested.emit(self)
557 self.exit_requested.emit(self)
554
558
555 def _handle_payload_next_input(self, item):
559 def _handle_payload_next_input(self, item):
556 self.input_buffer = item['text']
560 self.input_buffer = item['text']
557
561
558 def _handle_payload_page(self, item):
562 def _handle_payload_page(self, item):
559 # Since the plain text widget supports only a very small subset of HTML
563 # Since the plain text widget supports only a very small subset of HTML
560 # and we have no control over the HTML source, we only page HTML
564 # and we have no control over the HTML source, we only page HTML
561 # payloads in the rich text widget.
565 # payloads in the rich text widget.
562 if item['html'] and self.kind == 'rich':
566 if item['html'] and self.kind == 'rich':
563 self._page(item['html'], html=True)
567 self._page(item['html'], html=True)
564 else:
568 else:
565 self._page(item['text'], html=False)
569 self._page(item['text'], html=False)
566
570
567 #------ Trait change handlers --------------------------------------------
571 #------ Trait change handlers --------------------------------------------
568
572
569 def _style_sheet_changed(self):
573 def _style_sheet_changed(self):
570 """ Set the style sheets of the underlying widgets.
574 """ Set the style sheets of the underlying widgets.
571 """
575 """
572 self.setStyleSheet(self.style_sheet)
576 self.setStyleSheet(self.style_sheet)
573 if self._control is not None:
577 if self._control is not None:
574 self._control.document().setDefaultStyleSheet(self.style_sheet)
578 self._control.document().setDefaultStyleSheet(self.style_sheet)
575 bg_color = self._control.palette().window().color()
579 bg_color = self._control.palette().window().color()
576 self._ansi_processor.set_background_color(bg_color)
580 self._ansi_processor.set_background_color(bg_color)
577
581
578 if self._page_control is not None:
582 if self._page_control is not None:
579 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
583 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
580
584
581
585
582
586
583 def _syntax_style_changed(self):
587 def _syntax_style_changed(self):
584 """ Set the style for the syntax highlighter.
588 """ Set the style for the syntax highlighter.
585 """
589 """
586 if self._highlighter is None:
590 if self._highlighter is None:
587 # ignore premature calls
591 # ignore premature calls
588 return
592 return
589 if self.syntax_style:
593 if self.syntax_style:
590 self._highlighter.set_style(self.syntax_style)
594 self._highlighter.set_style(self.syntax_style)
591 else:
595 else:
592 self._highlighter.set_style_sheet(self.style_sheet)
596 self._highlighter.set_style_sheet(self.style_sheet)
593
597
594 #------ Trait default initializers -----------------------------------------
598 #------ Trait default initializers -----------------------------------------
595
599
596 def _banner_default(self):
600 def _banner_default(self):
597 from IPython.core.usage import default_gui_banner
601 from IPython.core.usage import default_gui_banner
598 return default_gui_banner
602 return default_gui_banner
General Comments 0
You need to be logged in to leave comments. Login now