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