##// END OF EJS Templates
BUG: execute_file fix failed on Windows....
Jonathan March -
Show More
@@ -1,551 +1,559
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. 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:
280 # or quotes.
281
282 # In earlier code here, to minimize escaping, we sometimes quoted the
283 # filename with single quotes. But to do this, this code must be
284 # platform-aware, because run uses shlex rather than python string
285 # parsing, so that:
286 # * In Win: single quotes can be used in the filename without quoting,
287 # and we cannot use single quotes to quote the filename.
288 # * In *nix: we can escape double quotes in a double quoted filename,
289 # but can't escape single quotes in a single quoted filename.
290
291 # So to keep this code non-platform-specific and simple, we now only
292 # use double quotes to quote filenames, and escape when needed:
293 if ' ' in path or "'" in path or '"' in path:
284 294 path = '"%s"' % path.replace('"', '\\"')
285 elif ' ' in path or '"' in path:
286 path = "'%s'" % path
287 295 self.execute('%%run %s' % path, hidden=hidden)
288 296
289 297 #---------------------------------------------------------------------------
290 298 # 'FrontendWidget' protected interface
291 299 #---------------------------------------------------------------------------
292 300
293 301 def _complete(self):
294 302 """ Reimplemented to support IPython's improved completion machinery.
295 303 """
296 304 # We let the kernel split the input line, so we *always* send an empty
297 305 # text field. Readline-based frontends do get a real text field which
298 306 # they can use.
299 307 text = ''
300 308
301 309 # Send the completion request to the kernel
302 310 msg_id = self.kernel_manager.shell_channel.complete(
303 311 text, # text
304 312 self._get_input_buffer_cursor_line(), # line
305 313 self._get_input_buffer_cursor_column(), # cursor_pos
306 314 self.input_buffer) # block
307 315 pos = self._get_cursor().position()
308 316 info = self._CompletionRequest(msg_id, pos)
309 317 self._request_info['complete'] = info
310 318
311 319 def _process_execute_error(self, msg):
312 320 """ Reimplemented for IPython-style traceback formatting.
313 321 """
314 322 content = msg['content']
315 323 traceback = '\n'.join(content['traceback']) + '\n'
316 324 if False:
317 325 # FIXME: For now, tracebacks come as plain text, so we can't use
318 326 # the html renderer yet. Once we refactor ultratb to produce
319 327 # properly styled tracebacks, this branch should be the default
320 328 traceback = traceback.replace(' ', '&nbsp;')
321 329 traceback = traceback.replace('\n', '<br/>')
322 330
323 331 ename = content['ename']
324 332 ename_styled = '<span class="error">%s</span>' % ename
325 333 traceback = traceback.replace(ename, ename_styled)
326 334
327 335 self._append_html(traceback)
328 336 else:
329 337 # This is the fallback for now, using plain text with ansi escapes
330 338 self._append_plain_text(traceback)
331 339
332 340 def _process_execute_payload(self, item):
333 341 """ Reimplemented to dispatch payloads to handler methods.
334 342 """
335 343 handler = self._payload_handlers.get(item['source'])
336 344 if handler is None:
337 345 # We have no handler for this type of payload, simply ignore it
338 346 return False
339 347 else:
340 348 handler(item)
341 349 return True
342 350
343 351 def _show_interpreter_prompt(self, number=None):
344 352 """ Reimplemented for IPython-style prompts.
345 353 """
346 354 # If a number was not specified, make a prompt number request.
347 355 if number is None:
348 356 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
349 357 info = self._ExecutionRequest(msg_id, 'prompt')
350 358 self._request_info['execute'][msg_id] = info
351 359 return
352 360
353 361 # Show a new prompt and save information about it so that it can be
354 362 # updated later if the prompt number turns out to be wrong.
355 363 self._prompt_sep = self.input_sep
356 364 self._show_prompt(self._make_in_prompt(number), html=True)
357 365 block = self._control.document().lastBlock()
358 366 length = len(self._prompt)
359 367 self._previous_prompt_obj = self._PromptBlock(block, length, number)
360 368
361 369 # Update continuation prompt to reflect (possibly) new prompt length.
362 370 self._set_continuation_prompt(
363 371 self._make_continuation_prompt(self._prompt), html=True)
364 372
365 373 def _show_interpreter_prompt_for_reply(self, msg):
366 374 """ Reimplemented for IPython-style prompts.
367 375 """
368 376 # Update the old prompt number if necessary.
369 377 content = msg['content']
370 378 # abort replies do not have any keys:
371 379 if content['status'] == 'aborted':
372 380 if self._previous_prompt_obj:
373 381 previous_prompt_number = self._previous_prompt_obj.number
374 382 else:
375 383 previous_prompt_number = 0
376 384 else:
377 385 previous_prompt_number = content['execution_count']
378 386 if self._previous_prompt_obj and \
379 387 self._previous_prompt_obj.number != previous_prompt_number:
380 388 block = self._previous_prompt_obj.block
381 389
382 390 # Make sure the prompt block has not been erased.
383 391 if block.isValid() and block.text():
384 392
385 393 # Remove the old prompt and insert a new prompt.
386 394 cursor = QtGui.QTextCursor(block)
387 395 cursor.movePosition(QtGui.QTextCursor.Right,
388 396 QtGui.QTextCursor.KeepAnchor,
389 397 self._previous_prompt_obj.length)
390 398 prompt = self._make_in_prompt(previous_prompt_number)
391 399 self._prompt = self._insert_html_fetching_plain_text(
392 400 cursor, prompt)
393 401
394 402 # When the HTML is inserted, Qt blows away the syntax
395 403 # highlighting for the line, so we need to rehighlight it.
396 404 self._highlighter.rehighlightBlock(cursor.block())
397 405
398 406 self._previous_prompt_obj = None
399 407
400 408 # Show a new prompt with the kernel's estimated prompt number.
401 409 self._show_interpreter_prompt(previous_prompt_number + 1)
402 410
403 411 #---------------------------------------------------------------------------
404 412 # 'IPythonWidget' interface
405 413 #---------------------------------------------------------------------------
406 414
407 415 def set_default_style(self, colors='lightbg'):
408 416 """ Sets the widget style to the class defaults.
409 417
410 418 Parameters:
411 419 -----------
412 420 colors : str, optional (default lightbg)
413 421 Whether to use the default IPython light background or dark
414 422 background or B&W style.
415 423 """
416 424 colors = colors.lower()
417 425 if colors=='lightbg':
418 426 self.style_sheet = styles.default_light_style_sheet
419 427 self.syntax_style = styles.default_light_syntax_style
420 428 elif colors=='linux':
421 429 self.style_sheet = styles.default_dark_style_sheet
422 430 self.syntax_style = styles.default_dark_syntax_style
423 431 elif colors=='nocolor':
424 432 self.style_sheet = styles.default_bw_style_sheet
425 433 self.syntax_style = styles.default_bw_syntax_style
426 434 else:
427 435 raise KeyError("No such color scheme: %s"%colors)
428 436
429 437 #---------------------------------------------------------------------------
430 438 # 'IPythonWidget' protected interface
431 439 #---------------------------------------------------------------------------
432 440
433 441 def _edit(self, filename, line=None):
434 442 """ Opens a Python script for editing.
435 443
436 444 Parameters:
437 445 -----------
438 446 filename : str
439 447 A path to a local system file.
440 448
441 449 line : int, optional
442 450 A line of interest in the file.
443 451 """
444 452 if self.custom_edit:
445 453 self.custom_edit_requested.emit(filename, line)
446 454 elif not self.editor:
447 455 self._append_plain_text('No default editor available.\n'
448 456 'Specify a GUI text editor in the `IPythonWidget.editor` '
449 457 'configurable to enable the %edit magic')
450 458 else:
451 459 try:
452 460 filename = '"%s"' % filename
453 461 if line and self.editor_line:
454 462 command = self.editor_line.format(filename=filename,
455 463 line=line)
456 464 else:
457 465 try:
458 466 command = self.editor.format()
459 467 except KeyError:
460 468 command = self.editor.format(filename=filename)
461 469 else:
462 470 command += ' ' + filename
463 471 except KeyError:
464 472 self._append_plain_text('Invalid editor command.\n')
465 473 else:
466 474 try:
467 475 Popen(command, shell=True)
468 476 except OSError:
469 477 msg = 'Opening editor with command "%s" failed.\n'
470 478 self._append_plain_text(msg % command)
471 479
472 480 def _make_in_prompt(self, number):
473 481 """ Given a prompt number, returns an HTML In prompt.
474 482 """
475 483 try:
476 484 body = self.in_prompt % number
477 485 except TypeError:
478 486 # allow in_prompt to leave out number, e.g. '>>> '
479 487 body = self.in_prompt
480 488 return '<span class="in-prompt">%s</span>' % body
481 489
482 490 def _make_continuation_prompt(self, prompt):
483 491 """ Given a plain text version of an In prompt, returns an HTML
484 492 continuation prompt.
485 493 """
486 494 end_chars = '...: '
487 495 space_count = len(prompt.lstrip('\n')) - len(end_chars)
488 496 body = '&nbsp;' * space_count + end_chars
489 497 return '<span class="in-prompt">%s</span>' % body
490 498
491 499 def _make_out_prompt(self, number):
492 500 """ Given a prompt number, returns an HTML Out prompt.
493 501 """
494 502 body = self.out_prompt % number
495 503 return '<span class="out-prompt">%s</span>' % body
496 504
497 505 #------ Payload handlers --------------------------------------------------
498 506
499 507 # Payload handlers with a generic interface: each takes the opaque payload
500 508 # dict, unpacks it and calls the underlying functions with the necessary
501 509 # arguments.
502 510
503 511 def _handle_payload_edit(self, item):
504 512 self._edit(item['filename'], item['line_number'])
505 513
506 514 def _handle_payload_exit(self, item):
507 515 self._keep_kernel_on_exit = item['keepkernel']
508 516 self.exit_requested.emit(self)
509 517
510 518 def _handle_payload_next_input(self, item):
511 519 self.input_buffer = dedent(item['text'].rstrip())
512 520
513 521 def _handle_payload_page(self, item):
514 522 # Since the plain text widget supports only a very small subset of HTML
515 523 # and we have no control over the HTML source, we only page HTML
516 524 # payloads in the rich text widget.
517 525 if item['html'] and self.kind == 'rich':
518 526 self._page(item['html'], html=True)
519 527 else:
520 528 self._page(item['text'], html=False)
521 529
522 530 #------ Trait change handlers --------------------------------------------
523 531
524 532 def _style_sheet_changed(self):
525 533 """ Set the style sheets of the underlying widgets.
526 534 """
527 535 self.setStyleSheet(self.style_sheet)
528 536 self._control.document().setDefaultStyleSheet(self.style_sheet)
529 537 if self._page_control:
530 538 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
531 539
532 540 bg_color = self._control.palette().window().color()
533 541 self._ansi_processor.set_background_color(bg_color)
534 542
535 543
536 544 def _syntax_style_changed(self):
537 545 """ Set the style for the syntax highlighter.
538 546 """
539 547 if self._highlighter is None:
540 548 # ignore premature calls
541 549 return
542 550 if self.syntax_style:
543 551 self._highlighter.set_style(self.syntax_style)
544 552 else:
545 553 self._highlighter.set_style_sheet(self.style_sheet)
546 554
547 555 #------ Trait default initializers -----------------------------------------
548 556
549 557 def _banner_default(self):
550 558 from IPython.core.usage import default_gui_banner
551 559 return default_gui_banner
General Comments 0
You need to be logged in to leave comments. Login now