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