##// END OF EJS Templates
Reimplemented IPythonWidget's edit magic handling to support line numbers. Also, removed the code path for launching the file with the system default Python application, as this is too dangerous.
epatters -
Show More
@@ -1,325 +1,343 b''
1 """ A FrontendWidget that emulates the interface of the console IPython and
2 supports the additional functionality provided by the IPython kernel.
3
4 TODO: Add support for retrieving the system default editor. Requires code
5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 Linux (use the xdg system).
7 """
8
1 # Standard library imports
9 # Standard library imports
2 from subprocess import Popen
10 from subprocess import Popen
3
11
4 # System library imports
12 # System library imports
5 from PyQt4 import QtCore, QtGui
13 from PyQt4 import QtCore, QtGui
6
14
7 # Local imports
15 # Local imports
8 from IPython.core.inputsplitter import IPythonInputSplitter
16 from IPython.core.inputsplitter import IPythonInputSplitter
9 from IPython.core.usage import default_banner
17 from IPython.core.usage import default_banner
10 from frontend_widget import FrontendWidget
18 from frontend_widget import FrontendWidget
11
19
12
20
13 class IPythonPromptBlock(object):
21 class IPythonPromptBlock(object):
14 """ An internal storage object for IPythonWidget.
22 """ An internal storage object for IPythonWidget.
15 """
23 """
16 def __init__(self, block, length, number):
24 def __init__(self, block, length, number):
17 self.block = block
25 self.block = block
18 self.length = length
26 self.length = length
19 self.number = number
27 self.number = number
20
28
21
29
22 class IPythonWidget(FrontendWidget):
30 class IPythonWidget(FrontendWidget):
23 """ A FrontendWidget for an IPython kernel.
31 """ A FrontendWidget for an IPython kernel.
24 """
32 """
25
33
26 # Signal emitted when an editor is needed for a file and the editor has been
34 # Signal emitted when an editor is needed for a file and the editor has been
27 # specified as 'custom'. See 'set_editor' for more information.
35 # specified as 'custom'. See 'set_editor' for more information.
28 custom_edit_requested = QtCore.pyqtSignal(object, object)
36 custom_edit_requested = QtCore.pyqtSignal(object, object)
29
37
30 # The default stylesheet: black text on a white background.
38 # The default stylesheet: black text on a white background.
31 default_stylesheet = """
39 default_stylesheet = """
32 .error { color: red; }
40 .error { color: red; }
33 .in-prompt { color: navy; }
41 .in-prompt { color: navy; }
34 .in-prompt-number { font-weight: bold; }
42 .in-prompt-number { font-weight: bold; }
35 .out-prompt { color: darkred; }
43 .out-prompt { color: darkred; }
36 .out-prompt-number { font-weight: bold; }
44 .out-prompt-number { font-weight: bold; }
37 """
45 """
38
46
39 # A dark stylesheet: white text on a black background.
47 # A dark stylesheet: white text on a black background.
40 dark_stylesheet = """
48 dark_stylesheet = """
41 QPlainTextEdit, QTextEdit { background-color: black; color: white }
49 QPlainTextEdit, QTextEdit { background-color: black; color: white }
42 QFrame { border: 1px solid grey; }
50 QFrame { border: 1px solid grey; }
43 .error { color: red; }
51 .error { color: red; }
44 .in-prompt { color: lime; }
52 .in-prompt { color: lime; }
45 .in-prompt-number { color: lime; font-weight: bold; }
53 .in-prompt-number { color: lime; font-weight: bold; }
46 .out-prompt { color: red; }
54 .out-prompt { color: red; }
47 .out-prompt-number { color: red; font-weight: bold; }
55 .out-prompt-number { color: red; font-weight: bold; }
48 """
56 """
49
57
50 # Default prompts.
58 # Default prompts.
51 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
59 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
52 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
60 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
53
61
54 # FrontendWidget protected class variables.
62 # FrontendWidget protected class variables.
55 #_input_splitter_class = IPythonInputSplitter
63 #_input_splitter_class = IPythonInputSplitter
56
64
57 # IPythonWidget protected class variables.
65 # IPythonWidget protected class variables.
58 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
66 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
59 _payload_source_page = 'IPython.zmq.page.page'
67 _payload_source_page = 'IPython.zmq.page.page'
60
68
61 #---------------------------------------------------------------------------
69 #---------------------------------------------------------------------------
62 # 'object' interface
70 # 'object' interface
63 #---------------------------------------------------------------------------
71 #---------------------------------------------------------------------------
64
72
65 def __init__(self, *args, **kw):
73 def __init__(self, *args, **kw):
66 super(IPythonWidget, self).__init__(*args, **kw)
74 super(IPythonWidget, self).__init__(*args, **kw)
67
75
68 # IPythonWidget protected variables.
76 # IPythonWidget protected variables.
69 self._previous_prompt_obj = None
77 self._previous_prompt_obj = None
70
78
71 # Set a default editor and stylesheet.
79 # Set a default editor and stylesheet.
72 self.set_editor('default')
80 self.set_editor('default')
73 self.reset_styling()
81 self.reset_styling()
74
82
75 #---------------------------------------------------------------------------
83 #---------------------------------------------------------------------------
76 # 'BaseFrontendMixin' abstract interface
84 # 'BaseFrontendMixin' abstract interface
77 #---------------------------------------------------------------------------
85 #---------------------------------------------------------------------------
78
86
79 def _handle_history_reply(self, msg):
87 def _handle_history_reply(self, msg):
80 """ Implemented to handle history replies, which are only supported by
88 """ Implemented to handle history replies, which are only supported by
81 the IPython kernel.
89 the IPython kernel.
82 """
90 """
83 history_dict = msg['content']['history']
91 history_dict = msg['content']['history']
84 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
92 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
85 self._set_history(items)
93 self._set_history(items)
86
94
87 def _handle_prompt_reply(self, msg):
95 def _handle_prompt_reply(self, msg):
88 """ Implemented to handle prompt number replies, which are only
96 """ Implemented to handle prompt number replies, which are only
89 supported by the IPython kernel.
97 supported by the IPython kernel.
90 """
98 """
91 content = msg['content']
99 content = msg['content']
92 self._show_interpreter_prompt(content['prompt_number'],
100 self._show_interpreter_prompt(content['prompt_number'],
93 content['input_sep'])
101 content['input_sep'])
94
102
95 def _handle_pyout(self, msg):
103 def _handle_pyout(self, msg):
96 """ Reimplemented for IPython-style "display hook".
104 """ Reimplemented for IPython-style "display hook".
97 """
105 """
98 if not self._hidden and self._is_from_this_session(msg):
106 if not self._hidden and self._is_from_this_session(msg):
99 content = msg['content']
107 content = msg['content']
100 prompt_number = content['prompt_number']
108 prompt_number = content['prompt_number']
101 self._append_plain_text(content['output_sep'])
109 self._append_plain_text(content['output_sep'])
102 self._append_html(self._make_out_prompt(prompt_number))
110 self._append_html(self._make_out_prompt(prompt_number))
103 self._append_plain_text(content['data'] + '\n' +
111 self._append_plain_text(content['data'] + '\n' +
104 content['output_sep2'])
112 content['output_sep2'])
105
113
106 def _started_channels(self):
114 def _started_channels(self):
107 """ Reimplemented to make a history request.
115 """ Reimplemented to make a history request.
108 """
116 """
109 super(IPythonWidget, self)._started_channels()
117 super(IPythonWidget, self)._started_channels()
110 # FIXME: Disabled until history requests are properly implemented.
118 # FIXME: Disabled until history requests are properly implemented.
111 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
119 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
112
120
113 #---------------------------------------------------------------------------
121 #---------------------------------------------------------------------------
114 # 'FrontendWidget' interface
122 # 'FrontendWidget' interface
115 #---------------------------------------------------------------------------
123 #---------------------------------------------------------------------------
116
124
117 def execute_file(self, path, hidden=False):
125 def execute_file(self, path, hidden=False):
118 """ Reimplemented to use the 'run' magic.
126 """ Reimplemented to use the 'run' magic.
119 """
127 """
120 self.execute('%%run %s' % path, hidden=hidden)
128 self.execute('%%run %s' % path, hidden=hidden)
121
129
122 #---------------------------------------------------------------------------
130 #---------------------------------------------------------------------------
123 # 'FrontendWidget' protected interface
131 # 'FrontendWidget' protected interface
124 #---------------------------------------------------------------------------
132 #---------------------------------------------------------------------------
125
133
126 def _get_banner(self):
134 def _get_banner(self):
127 """ Reimplemented to return IPython's default banner.
135 """ Reimplemented to return IPython's default banner.
128 """
136 """
129 return default_banner + '\n'
137 return default_banner + '\n'
130
138
131 def _process_execute_error(self, msg):
139 def _process_execute_error(self, msg):
132 """ Reimplemented for IPython-style traceback formatting.
140 """ Reimplemented for IPython-style traceback formatting.
133 """
141 """
134 content = msg['content']
142 content = msg['content']
135 traceback = '\n'.join(content['traceback']) + '\n'
143 traceback = '\n'.join(content['traceback']) + '\n'
136 if False:
144 if False:
137 # FIXME: For now, tracebacks come as plain text, so we can't use
145 # FIXME: For now, tracebacks come as plain text, so we can't use
138 # the html renderer yet. Once we refactor ultratb to produce
146 # the html renderer yet. Once we refactor ultratb to produce
139 # properly styled tracebacks, this branch should be the default
147 # properly styled tracebacks, this branch should be the default
140 traceback = traceback.replace(' ', '&nbsp;')
148 traceback = traceback.replace(' ', '&nbsp;')
141 traceback = traceback.replace('\n', '<br/>')
149 traceback = traceback.replace('\n', '<br/>')
142
150
143 ename = content['ename']
151 ename = content['ename']
144 ename_styled = '<span class="error">%s</span>' % ename
152 ename_styled = '<span class="error">%s</span>' % ename
145 traceback = traceback.replace(ename, ename_styled)
153 traceback = traceback.replace(ename, ename_styled)
146
154
147 self._append_html(traceback)
155 self._append_html(traceback)
148 else:
156 else:
149 # This is the fallback for now, using plain text with ansi escapes
157 # This is the fallback for now, using plain text with ansi escapes
150 self._append_plain_text(traceback)
158 self._append_plain_text(traceback)
151
159
152 def _process_execute_payload(self, item):
160 def _process_execute_payload(self, item):
153 """ Reimplemented to handle %edit and paging payloads.
161 """ Reimplemented to handle %edit and paging payloads.
154 """
162 """
155 if item['source'] == self._payload_source_edit:
163 if item['source'] == self._payload_source_edit:
156 self.edit(item['filename'], item['line_number'])
164 self._edit(item['filename'], item['line_number'])
157 return True
165 return True
158 elif item['source'] == self._payload_source_page:
166 elif item['source'] == self._payload_source_page:
159 self._page(item['data'])
167 self._page(item['data'])
160 return True
168 return True
161 else:
169 else:
162 return False
170 return False
163
171
164 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
172 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
165 """ Reimplemented for IPython-style prompts.
173 """ Reimplemented for IPython-style prompts.
166 """
174 """
167 # If a number was not specified, make a prompt number request.
175 # If a number was not specified, make a prompt number request.
168 if number is None:
176 if number is None:
169 self.kernel_manager.xreq_channel.prompt()
177 self.kernel_manager.xreq_channel.prompt()
170 return
178 return
171
179
172 # Show a new prompt and save information about it so that it can be
180 # Show a new prompt and save information about it so that it can be
173 # updated later if the prompt number turns out to be wrong.
181 # updated later if the prompt number turns out to be wrong.
174 self._append_plain_text(input_sep)
182 self._append_plain_text(input_sep)
175 self._show_prompt(self._make_in_prompt(number), html=True)
183 self._show_prompt(self._make_in_prompt(number), html=True)
176 block = self._control.document().lastBlock()
184 block = self._control.document().lastBlock()
177 length = len(self._prompt)
185 length = len(self._prompt)
178 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
186 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
179
187
180 # Update continuation prompt to reflect (possibly) new prompt length.
188 # Update continuation prompt to reflect (possibly) new prompt length.
181 self._set_continuation_prompt(
189 self._set_continuation_prompt(
182 self._make_continuation_prompt(self._prompt), html=True)
190 self._make_continuation_prompt(self._prompt), html=True)
183
191
184 def _show_interpreter_prompt_for_reply(self, msg):
192 def _show_interpreter_prompt_for_reply(self, msg):
185 """ Reimplemented for IPython-style prompts.
193 """ Reimplemented for IPython-style prompts.
186 """
194 """
187 # Update the old prompt number if necessary.
195 # Update the old prompt number if necessary.
188 content = msg['content']
196 content = msg['content']
189 previous_prompt_number = content['prompt_number']
197 previous_prompt_number = content['prompt_number']
190 if self._previous_prompt_obj and \
198 if self._previous_prompt_obj and \
191 self._previous_prompt_obj.number != previous_prompt_number:
199 self._previous_prompt_obj.number != previous_prompt_number:
192 block = self._previous_prompt_obj.block
200 block = self._previous_prompt_obj.block
193
201
194 # Make sure the prompt block has not been erased.
202 # Make sure the prompt block has not been erased.
195 if block.isValid() and not block.text().isEmpty():
203 if block.isValid() and not block.text().isEmpty():
196
204
197 # Remove the old prompt and insert a new prompt.
205 # Remove the old prompt and insert a new prompt.
198 cursor = QtGui.QTextCursor(block)
206 cursor = QtGui.QTextCursor(block)
199 cursor.movePosition(QtGui.QTextCursor.Right,
207 cursor.movePosition(QtGui.QTextCursor.Right,
200 QtGui.QTextCursor.KeepAnchor,
208 QtGui.QTextCursor.KeepAnchor,
201 self._previous_prompt_obj.length)
209 self._previous_prompt_obj.length)
202 prompt = self._make_in_prompt(previous_prompt_number)
210 prompt = self._make_in_prompt(previous_prompt_number)
203 self._prompt = self._insert_html_fetching_plain_text(
211 self._prompt = self._insert_html_fetching_plain_text(
204 cursor, prompt)
212 cursor, prompt)
205
213
206 # When the HTML is inserted, Qt blows away the syntax
214 # When the HTML is inserted, Qt blows away the syntax
207 # highlighting for the line, so we need to rehighlight it.
215 # highlighting for the line, so we need to rehighlight it.
208 self._highlighter.rehighlightBlock(cursor.block())
216 self._highlighter.rehighlightBlock(cursor.block())
209
217
210 self._previous_prompt_obj = None
218 self._previous_prompt_obj = None
211
219
212 # Show a new prompt with the kernel's estimated prompt number.
220 # Show a new prompt with the kernel's estimated prompt number.
213 next_prompt = content['next_prompt']
221 next_prompt = content['next_prompt']
214 self._show_interpreter_prompt(next_prompt['prompt_number'],
222 self._show_interpreter_prompt(next_prompt['prompt_number'],
215 next_prompt['input_sep'])
223 next_prompt['input_sep'])
216
224
217 #---------------------------------------------------------------------------
225 #---------------------------------------------------------------------------
218 # 'IPythonWidget' interface
226 # 'IPythonWidget' interface
219 #---------------------------------------------------------------------------
227 #---------------------------------------------------------------------------
220
228
221 def edit(self, filename, line=None):
222 """ Opens a Python script for editing.
223
224 Parameters:
225 -----------
226 filename : str
227 A path to a local system file.
228
229 line : int, optional
230 A line of interest in the file.
231
232 Raises:
233 -------
234 OSError
235 If the editor command cannot be executed.
236 """
237 if self._editor == 'default':
238 url = QtCore.QUrl.fromLocalFile(filename)
239 if not QtGui.QDesktopServices.openUrl(url):
240 message = 'Failed to open %s with the default application'
241 raise OSError(message % repr(filename))
242 elif self._editor is None:
243 self.custom_edit_requested.emit(filename, line)
244 else:
245 Popen(self._editor + [filename])
246
247 def reset_styling(self):
229 def reset_styling(self):
248 """ Restores the default IPythonWidget styling.
230 """ Restores the default IPythonWidget styling.
249 """
231 """
250 self.set_styling(self.default_stylesheet, syntax_style='default')
232 self.set_styling(self.default_stylesheet, syntax_style='default')
251 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
233 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
252
234
253 def set_editor(self, editor):
235 def set_editor(self, editor, line_editor=None):
254 """ Sets the editor to use with the %edit magic.
236 """ Sets the editor to use with the %edit magic.
255
237
256 Parameters:
238 Parameters:
257 -----------
239 -----------
258 editor : str or sequence of str
240 editor : str
259 A command suitable for use with Popen. This command will be executed
241 A command for invoking a system text editor. If the string contains
260 with a single argument--a filename--when editing is requested.
242 a {filename} format specifier, it will be used. Otherwise, the
243 filename will be appended to the end the command.
261
244
262 This parameter also takes two special values:
245 This parameter also takes a special value:
263 'default' : Files will be edited with the system default
264 application for Python files.
265 'custom' : Emit a 'custom_edit_requested(str, int)' signal
246 'custom' : Emit a 'custom_edit_requested(str, int)' signal
266 instead of opening an editor.
247 instead of opening an editor.
248
249 line_editor : str, optional
250 The editor command to use when a specific line number is
251 requested. The string should contain two format specifiers: {line}
252 and {filename}. If this parameter is not specified, the line number
253 option to the %edit magic will be ignored.
267 """
254 """
268 if editor == 'default':
255 self._editor = editor
269 self._editor = 'default'
256 self._editor_line = line_editor
270 elif editor == 'custom':
271 self._editor = None
272 elif isinstance(editor, basestring):
273 self._editor = [ editor ]
274 else:
275 self._editor = list(editor)
276
257
277 def set_styling(self, stylesheet, syntax_style=None):
258 def set_styling(self, stylesheet, syntax_style=None):
278 """ Sets the IPythonWidget styling.
259 """ Sets the IPythonWidget styling.
279
260
280 Parameters:
261 Parameters:
281 -----------
262 -----------
282 stylesheet : str
263 stylesheet : str
283 A CSS stylesheet. The stylesheet can contain classes for:
264 A CSS stylesheet. The stylesheet can contain classes for:
284 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
265 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
285 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
266 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
286 3. IPython: .error, .in-prompt, .out-prompt, etc.
267 3. IPython: .error, .in-prompt, .out-prompt, etc.
287
268
288 syntax_style : str or None [default None]
269 syntax_style : str or None [default None]
289 If specified, use the Pygments style with given name. Otherwise,
270 If specified, use the Pygments style with given name. Otherwise,
290 the stylesheet is queried for Pygments style information.
271 the stylesheet is queried for Pygments style information.
291 """
272 """
292 self.setStyleSheet(stylesheet)
273 self.setStyleSheet(stylesheet)
293 self._control.document().setDefaultStyleSheet(stylesheet)
274 self._control.document().setDefaultStyleSheet(stylesheet)
294 if self._page_control:
275 if self._page_control:
295 self._page_control.document().setDefaultStyleSheet(stylesheet)
276 self._page_control.document().setDefaultStyleSheet(stylesheet)
296
277
297 if syntax_style is None:
278 if syntax_style is None:
298 self._highlighter.set_style_sheet(stylesheet)
279 self._highlighter.set_style_sheet(stylesheet)
299 else:
280 else:
300 self._highlighter.set_style(syntax_style)
281 self._highlighter.set_style(syntax_style)
301
282
302 #---------------------------------------------------------------------------
283 #---------------------------------------------------------------------------
303 # 'IPythonWidget' protected interface
284 # 'IPythonWidget' protected interface
304 #---------------------------------------------------------------------------
285 #---------------------------------------------------------------------------
305
286
287 def _edit(self, filename, line=None):
288 """ Opens a Python script for editing.
289
290 Parameters:
291 -----------
292 filename : str
293 A path to a local system file.
294
295 line : int, optional
296 A line of interest in the file.
297 """
298 if self._editor == 'custom':
299 self.custom_edit_requested.emit(filename, line)
300 elif self._editor == 'default':
301 self._append_plain_text('No default editor available.\n')
302 else:
303 try:
304 filename = '"%s"' % filename
305 if line and self._editor_line:
306 command = self._editor_line.format(filename=filename,
307 line=line)
308 else:
309 try:
310 command = self._editor.format()
311 except KeyError:
312 command = self._editor.format(filename=filename)
313 else:
314 command += ' ' + filename
315 except KeyError:
316 self._append_plain_text('Invalid editor command.\n')
317 else:
318 try:
319 Popen(command, shell=True)
320 except OSError:
321 msg = 'Opening editor with command "%s" failed.\n'
322 self._append_plain_text(msg % command)
323
306 def _make_in_prompt(self, number):
324 def _make_in_prompt(self, number):
307 """ Given a prompt number, returns an HTML In prompt.
325 """ Given a prompt number, returns an HTML In prompt.
308 """
326 """
309 body = self.in_prompt % number
327 body = self.in_prompt % number
310 return '<span class="in-prompt">%s</span>' % body
328 return '<span class="in-prompt">%s</span>' % body
311
329
312 def _make_continuation_prompt(self, prompt):
330 def _make_continuation_prompt(self, prompt):
313 """ Given a plain text version of an In prompt, returns an HTML
331 """ Given a plain text version of an In prompt, returns an HTML
314 continuation prompt.
332 continuation prompt.
315 """
333 """
316 end_chars = '...: '
334 end_chars = '...: '
317 space_count = len(prompt.lstrip('\n')) - len(end_chars)
335 space_count = len(prompt.lstrip('\n')) - len(end_chars)
318 body = '&nbsp;' * space_count + end_chars
336 body = '&nbsp;' * space_count + end_chars
319 return '<span class="in-prompt">%s</span>' % body
337 return '<span class="in-prompt">%s</span>' % body
320
338
321 def _make_out_prompt(self, number):
339 def _make_out_prompt(self, number):
322 """ Given a prompt number, returns an HTML Out prompt.
340 """ Given a prompt number, returns an HTML Out prompt.
323 """
341 """
324 body = self.out_prompt % number
342 body = self.out_prompt % number
325 return '<span class="out-prompt">%s</span>' % body
343 return '<span class="out-prompt">%s</span>' % body
General Comments 0
You need to be logged in to leave comments. Login now