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