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