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