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