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