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