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