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