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