##// END OF EJS Templates
highlight input from other front ends...
MinRK -
Show More
@@ -1,591 +1,599 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 157 # Move the control's cursor to the desired end point
158 158 cursor_pos = self._get_input_buffer_cursor_pos()
159 159 if end < cursor_pos:
160 160 cursor.movePosition(QtGui.QTextCursor.Left,
161 161 n=(cursor_pos - end))
162 162 elif end > cursor_pos:
163 163 cursor.movePosition(QtGui.QTextCursor.Right,
164 164 n=(end - cursor_pos))
165 165 # This line actually applies the move to control's cursor
166 166 self._control.setTextCursor(cursor)
167 167
168 168 offset = end - start
169 169 # Move the local cursor object to the start of the match and
170 170 # complete.
171 171 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
172 172 self._complete_with_items(cursor, matches)
173 173
174 174 def _handle_execute_reply(self, msg):
175 175 """ Reimplemented to support prompt requests.
176 176 """
177 177 msg_id = msg['parent_header'].get('msg_id')
178 178 info = self._request_info['execute'].get(msg_id)
179 179 if info and info.kind == 'prompt':
180 180 content = msg['content']
181 181 if content['status'] == 'aborted':
182 182 self._show_interpreter_prompt()
183 183 else:
184 184 number = content['execution_count'] + 1
185 185 self._show_interpreter_prompt(number)
186 186 self._request_info['execute'].pop(msg_id)
187 187 else:
188 188 super(IPythonWidget, self)._handle_execute_reply(msg)
189 189
190 190 def _handle_history_reply(self, msg):
191 191 """ Implemented to handle history tail replies, which are only supported
192 192 by the IPython kernel.
193 193 """
194 194 content = msg['content']
195 195 if 'history' not in content:
196 196 self.log.error("History request failed: %r"%content)
197 197 if content.get('status', '') == 'aborted' and \
198 198 not self._retrying_history_request:
199 199 # a *different* action caused this request to be aborted, so
200 200 # we should try again.
201 201 self.log.error("Retrying aborted history request")
202 202 # prevent multiple retries of aborted requests:
203 203 self._retrying_history_request = True
204 204 # wait out the kernel's queue flush, which is currently timed at 0.1s
205 205 time.sleep(0.25)
206 206 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
207 207 else:
208 208 self._retrying_history_request = False
209 209 return
210 210 # reset retry flag
211 211 self._retrying_history_request = False
212 212 history_items = content['history']
213 213 self.log.debug("Received history reply with %i entries", len(history_items))
214 214 items = []
215 215 last_cell = u""
216 216 for _, _, cell in history_items:
217 217 cell = cell.rstrip()
218 218 if cell != last_cell:
219 219 items.append(cell)
220 220 last_cell = cell
221 221 self._set_history(items)
222 222
223 def _insert_other_input(self, cursor, content):
224 """Insert function for input from other frontends"""
225 cursor.beginEditBlock()
226 start = cursor.position()
227 n = content.get('execution_count', 0)
228 cursor.insertText('\n')
229 self._insert_html(cursor, self._make_in_prompt(n))
230 cursor.insertText(content['code'])
231 self._highlighter.rehighlightBlock(cursor.block())
232 cursor.endEditBlock()
233
223 234 def _handle_execute_input(self, msg):
224 235 """Handle an execute_input message"""
225 236 self.log.debug("execute_input: %s", msg.get('content', ''))
226 237 if self.include_output(msg):
227 content = msg['content']
228 prompt_number = content.get('execution_count', 0)
229 self._append_html(self._make_in_prompt(prompt_number), True)
230 self._append_plain_text(content['code'], True)
238 self._append_custom(self._insert_other_input, msg['content'], before_prompt=True)
231 239
232 240
233 241 def _handle_execute_result(self, msg):
234 242 """ Reimplemented for IPython-style "display hook".
235 243 """
236 244 self.log.debug("execute_result: %s", msg.get('content', ''))
237 245 if self.include_output(msg):
238 246 self.flush_clearoutput()
239 247 content = msg['content']
240 248 prompt_number = content.get('execution_count', 0)
241 249 data = content['data']
242 250 if 'text/plain' in data:
243 251 self._append_plain_text(self.output_sep, True)
244 252 self._append_html(self._make_out_prompt(prompt_number), True)
245 253 text = data['text/plain']
246 254 # If the repr is multiline, make sure we start on a new line,
247 255 # so that its lines are aligned.
248 256 if "\n" in text and not self.output_sep.endswith("\n"):
249 257 self._append_plain_text('\n', True)
250 258 self._append_plain_text(text + self.output_sep2, True)
251 259
252 260 def _handle_display_data(self, msg):
253 261 """ The base handler for the ``display_data`` message.
254 262 """
255 263 self.log.debug("display: %s", msg.get('content', ''))
256 264 # For now, we don't display data from other frontends, but we
257 265 # eventually will as this allows all frontends to monitor the display
258 266 # data. But we need to figure out how to handle this in the GUI.
259 267 if self.include_output(msg):
260 268 self.flush_clearoutput()
261 269 data = msg['content']['data']
262 270 metadata = msg['content']['metadata']
263 271 # In the regular IPythonWidget, we simply print the plain text
264 272 # representation.
265 273 if 'text/plain' in data:
266 274 text = data['text/plain']
267 275 self._append_plain_text(text, True)
268 276 # This newline seems to be needed for text and html output.
269 277 self._append_plain_text(u'\n', True)
270 278
271 279 def _handle_kernel_info_reply(self, rep):
272 280 """Handle kernel info replies."""
273 281 content = rep['content']
274 282 if not self._guiref_loaded:
275 283 if content.get('language') == 'python':
276 284 self._load_guiref_magic()
277 285 self._guiref_loaded = True
278 286
279 287 self.kernel_banner = content.get('banner', '')
280 288 if self._starting:
281 289 # finish handling started channels
282 290 self._starting = False
283 291 super(IPythonWidget, self)._started_channels()
284 292
285 293 def _started_channels(self):
286 294 """Reimplemented to make a history request and load %guiref."""
287 295 self._starting = True
288 296 # The reply will trigger %guiref load provided language=='python'
289 297 self.kernel_client.kernel_info()
290 298
291 299 self.kernel_client.shell_channel.history(hist_access_type='tail',
292 300 n=1000)
293 301
294 302 def _load_guiref_magic(self):
295 303 """Load %guiref magic."""
296 304 self.kernel_client.shell_channel.execute('\n'.join([
297 305 "try:",
298 306 " _usage",
299 307 "except:",
300 308 " from IPython.core import usage as _usage",
301 309 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
302 310 " del _usage",
303 311 ]), silent=True)
304 312
305 313 #---------------------------------------------------------------------------
306 314 # 'ConsoleWidget' public interface
307 315 #---------------------------------------------------------------------------
308 316
309 317 #---------------------------------------------------------------------------
310 318 # 'FrontendWidget' public interface
311 319 #---------------------------------------------------------------------------
312 320
313 321 def execute_file(self, path, hidden=False):
314 322 """ Reimplemented to use the 'run' magic.
315 323 """
316 324 # Use forward slashes on Windows to avoid escaping each separator.
317 325 if sys.platform == 'win32':
318 326 path = os.path.normpath(path).replace('\\', '/')
319 327
320 328 # Perhaps we should not be using %run directly, but while we
321 329 # are, it is necessary to quote or escape filenames containing spaces
322 330 # or quotes.
323 331
324 332 # In earlier code here, to minimize escaping, we sometimes quoted the
325 333 # filename with single quotes. But to do this, this code must be
326 334 # platform-aware, because run uses shlex rather than python string
327 335 # parsing, so that:
328 336 # * In Win: single quotes can be used in the filename without quoting,
329 337 # and we cannot use single quotes to quote the filename.
330 338 # * In *nix: we can escape double quotes in a double quoted filename,
331 339 # but can't escape single quotes in a single quoted filename.
332 340
333 341 # So to keep this code non-platform-specific and simple, we now only
334 342 # use double quotes to quote filenames, and escape when needed:
335 343 if ' ' in path or "'" in path or '"' in path:
336 344 path = '"%s"' % path.replace('"', '\\"')
337 345 self.execute('%%run %s' % path, hidden=hidden)
338 346
339 347 #---------------------------------------------------------------------------
340 348 # 'FrontendWidget' protected interface
341 349 #---------------------------------------------------------------------------
342 350
343 351 def _process_execute_error(self, msg):
344 352 """ Reimplemented for IPython-style traceback formatting.
345 353 """
346 354 content = msg['content']
347 355 traceback = '\n'.join(content['traceback']) + '\n'
348 356 if False:
349 357 # FIXME: For now, tracebacks come as plain text, so we can't use
350 358 # the html renderer yet. Once we refactor ultratb to produce
351 359 # properly styled tracebacks, this branch should be the default
352 360 traceback = traceback.replace(' ', '&nbsp;')
353 361 traceback = traceback.replace('\n', '<br/>')
354 362
355 363 ename = content['ename']
356 364 ename_styled = '<span class="error">%s</span>' % ename
357 365 traceback = traceback.replace(ename, ename_styled)
358 366
359 367 self._append_html(traceback)
360 368 else:
361 369 # This is the fallback for now, using plain text with ansi escapes
362 370 self._append_plain_text(traceback)
363 371
364 372 def _process_execute_payload(self, item):
365 373 """ Reimplemented to dispatch payloads to handler methods.
366 374 """
367 375 handler = self._payload_handlers.get(item['source'])
368 376 if handler is None:
369 377 # We have no handler for this type of payload, simply ignore it
370 378 return False
371 379 else:
372 380 handler(item)
373 381 return True
374 382
375 383 def _show_interpreter_prompt(self, number=None):
376 384 """ Reimplemented for IPython-style prompts.
377 385 """
378 386 # If a number was not specified, make a prompt number request.
379 387 if number is None:
380 388 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
381 389 info = self._ExecutionRequest(msg_id, 'prompt')
382 390 self._request_info['execute'][msg_id] = info
383 391 return
384 392
385 393 # Show a new prompt and save information about it so that it can be
386 394 # updated later if the prompt number turns out to be wrong.
387 395 self._prompt_sep = self.input_sep
388 396 self._show_prompt(self._make_in_prompt(number), html=True)
389 397 block = self._control.document().lastBlock()
390 398 length = len(self._prompt)
391 399 self._previous_prompt_obj = self._PromptBlock(block, length, number)
392 400
393 401 # Update continuation prompt to reflect (possibly) new prompt length.
394 402 self._set_continuation_prompt(
395 403 self._make_continuation_prompt(self._prompt), html=True)
396 404
397 405 def _show_interpreter_prompt_for_reply(self, msg):
398 406 """ Reimplemented for IPython-style prompts.
399 407 """
400 408 # Update the old prompt number if necessary.
401 409 content = msg['content']
402 410 # abort replies do not have any keys:
403 411 if content['status'] == 'aborted':
404 412 if self._previous_prompt_obj:
405 413 previous_prompt_number = self._previous_prompt_obj.number
406 414 else:
407 415 previous_prompt_number = 0
408 416 else:
409 417 previous_prompt_number = content['execution_count']
410 418 if self._previous_prompt_obj and \
411 419 self._previous_prompt_obj.number != previous_prompt_number:
412 420 block = self._previous_prompt_obj.block
413 421
414 422 # Make sure the prompt block has not been erased.
415 423 if block.isValid() and block.text():
416 424
417 425 # Remove the old prompt and insert a new prompt.
418 426 cursor = QtGui.QTextCursor(block)
419 427 cursor.movePosition(QtGui.QTextCursor.Right,
420 428 QtGui.QTextCursor.KeepAnchor,
421 429 self._previous_prompt_obj.length)
422 430 prompt = self._make_in_prompt(previous_prompt_number)
423 431 self._prompt = self._insert_html_fetching_plain_text(
424 432 cursor, prompt)
425 433
426 434 # When the HTML is inserted, Qt blows away the syntax
427 435 # highlighting for the line, so we need to rehighlight it.
428 436 self._highlighter.rehighlightBlock(cursor.block())
429 437
430 438 self._previous_prompt_obj = None
431 439
432 440 # Show a new prompt with the kernel's estimated prompt number.
433 441 self._show_interpreter_prompt(previous_prompt_number + 1)
434 442
435 443 #---------------------------------------------------------------------------
436 444 # 'IPythonWidget' interface
437 445 #---------------------------------------------------------------------------
438 446
439 447 def set_default_style(self, colors='lightbg'):
440 448 """ Sets the widget style to the class defaults.
441 449
442 450 Parameters
443 451 ----------
444 452 colors : str, optional (default lightbg)
445 453 Whether to use the default IPython light background or dark
446 454 background or B&W style.
447 455 """
448 456 colors = colors.lower()
449 457 if colors=='lightbg':
450 458 self.style_sheet = styles.default_light_style_sheet
451 459 self.syntax_style = styles.default_light_syntax_style
452 460 elif colors=='linux':
453 461 self.style_sheet = styles.default_dark_style_sheet
454 462 self.syntax_style = styles.default_dark_syntax_style
455 463 elif colors=='nocolor':
456 464 self.style_sheet = styles.default_bw_style_sheet
457 465 self.syntax_style = styles.default_bw_syntax_style
458 466 else:
459 467 raise KeyError("No such color scheme: %s"%colors)
460 468
461 469 #---------------------------------------------------------------------------
462 470 # 'IPythonWidget' protected interface
463 471 #---------------------------------------------------------------------------
464 472
465 473 def _edit(self, filename, line=None):
466 474 """ Opens a Python script for editing.
467 475
468 476 Parameters
469 477 ----------
470 478 filename : str
471 479 A path to a local system file.
472 480
473 481 line : int, optional
474 482 A line of interest in the file.
475 483 """
476 484 if self.custom_edit:
477 485 self.custom_edit_requested.emit(filename, line)
478 486 elif not self.editor:
479 487 self._append_plain_text('No default editor available.\n'
480 488 'Specify a GUI text editor in the `IPythonWidget.editor` '
481 489 'configurable to enable the %edit magic')
482 490 else:
483 491 try:
484 492 filename = '"%s"' % filename
485 493 if line and self.editor_line:
486 494 command = self.editor_line.format(filename=filename,
487 495 line=line)
488 496 else:
489 497 try:
490 498 command = self.editor.format()
491 499 except KeyError:
492 500 command = self.editor.format(filename=filename)
493 501 else:
494 502 command += ' ' + filename
495 503 except KeyError:
496 504 self._append_plain_text('Invalid editor command.\n')
497 505 else:
498 506 try:
499 507 Popen(command, shell=True)
500 508 except OSError:
501 509 msg = 'Opening editor with command "%s" failed.\n'
502 510 self._append_plain_text(msg % command)
503 511
504 512 def _make_in_prompt(self, number):
505 513 """ Given a prompt number, returns an HTML In prompt.
506 514 """
507 515 try:
508 516 body = self.in_prompt % number
509 517 except TypeError:
510 518 # allow in_prompt to leave out number, e.g. '>>> '
511 519 from xml.sax.saxutils import escape
512 520 body = escape(self.in_prompt)
513 521 return '<span class="in-prompt">%s</span>' % body
514 522
515 523 def _make_continuation_prompt(self, prompt):
516 524 """ Given a plain text version of an In prompt, returns an HTML
517 525 continuation prompt.
518 526 """
519 527 end_chars = '...: '
520 528 space_count = len(prompt.lstrip('\n')) - len(end_chars)
521 529 body = '&nbsp;' * space_count + end_chars
522 530 return '<span class="in-prompt">%s</span>' % body
523 531
524 532 def _make_out_prompt(self, number):
525 533 """ Given a prompt number, returns an HTML Out prompt.
526 534 """
527 535 try:
528 536 body = self.out_prompt % number
529 537 except TypeError:
530 538 # allow out_prompt to leave out number, e.g. '<<< '
531 539 from xml.sax.saxutils import escape
532 540 body = escape(self.out_prompt)
533 541 return '<span class="out-prompt">%s</span>' % body
534 542
535 543 #------ Payload handlers --------------------------------------------------
536 544
537 545 # Payload handlers with a generic interface: each takes the opaque payload
538 546 # dict, unpacks it and calls the underlying functions with the necessary
539 547 # arguments.
540 548
541 549 def _handle_payload_edit(self, item):
542 550 self._edit(item['filename'], item['line_number'])
543 551
544 552 def _handle_payload_exit(self, item):
545 553 self._keep_kernel_on_exit = item['keepkernel']
546 554 self.exit_requested.emit(self)
547 555
548 556 def _handle_payload_next_input(self, item):
549 557 self.input_buffer = item['text']
550 558
551 559 def _handle_payload_page(self, item):
552 560 # Since the plain text widget supports only a very small subset of HTML
553 561 # and we have no control over the HTML source, we only page HTML
554 562 # payloads in the rich text widget.
555 563 data = item['data']
556 564 if 'text/html' in data and self.kind == 'rich':
557 565 self._page(data['text/html'], html=True)
558 566 else:
559 567 self._page(data['text/plain'], html=False)
560 568
561 569 #------ Trait change handlers --------------------------------------------
562 570
563 571 def _style_sheet_changed(self):
564 572 """ Set the style sheets of the underlying widgets.
565 573 """
566 574 self.setStyleSheet(self.style_sheet)
567 575 if self._control is not None:
568 576 self._control.document().setDefaultStyleSheet(self.style_sheet)
569 577 bg_color = self._control.palette().window().color()
570 578 self._ansi_processor.set_background_color(bg_color)
571 579
572 580 if self._page_control is not None:
573 581 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
574 582
575 583
576 584
577 585 def _syntax_style_changed(self):
578 586 """ Set the style for the syntax highlighter.
579 587 """
580 588 if self._highlighter is None:
581 589 # ignore premature calls
582 590 return
583 591 if self.syntax_style:
584 592 self._highlighter.set_style(self.syntax_style)
585 593 else:
586 594 self._highlighter.set_style_sheet(self.style_sheet)
587 595
588 596 #------ Trait default initializers -----------------------------------------
589 597
590 598 def _banner_default(self):
591 599 return "IPython QtConsole {version}\n".format(version=version)
General Comments 0
You need to be logged in to leave comments. Login now