##// END OF EJS Templates
Make qtconsole aware of clear_output.
Jonathan Frederic -
Show More
@@ -1,806 +1,838 b''
1 1 from __future__ import print_function
2 2
3 3 # Standard library imports
4 4 from collections import namedtuple
5 5 import sys
6 6 import uuid
7 7
8 8 # System library imports
9 9 from IPython.external import qt
10 10 from IPython.external.qt import QtCore, QtGui
11 11 from IPython.utils import py3compat
12 12 from IPython.utils.importstring import import_item
13 13
14 14 # Local imports
15 15 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
16 16 from IPython.core.inputtransformer import classic_prompt
17 17 from IPython.core.oinspect import call_tip
18 18 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
19 19 from IPython.utils.traitlets import Any, Bool, Instance, Unicode, DottedObjectName
20 20 from .bracket_matcher import BracketMatcher
21 21 from .call_tip_widget import CallTipWidget
22 22 from .completion_lexer import CompletionLexer
23 23 from .history_console_widget import HistoryConsoleWidget
24 24 from .pygments_highlighter import PygmentsHighlighter
25 25
26 26
27 27 class FrontendHighlighter(PygmentsHighlighter):
28 28 """ A PygmentsHighlighter that understands and ignores prompts.
29 29 """
30 30
31 31 def __init__(self, frontend, lexer=None):
32 32 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
33 33 self._current_offset = 0
34 34 self._frontend = frontend
35 35 self.highlighting_on = False
36 36
37 37 def highlightBlock(self, string):
38 38 """ Highlight a block of text. Reimplemented to highlight selectively.
39 39 """
40 40 if not self.highlighting_on:
41 41 return
42 42
43 43 # The input to this function is a unicode string that may contain
44 44 # paragraph break characters, non-breaking spaces, etc. Here we acquire
45 45 # the string as plain text so we can compare it.
46 46 current_block = self.currentBlock()
47 47 string = self._frontend._get_block_plain_text(current_block)
48 48
49 49 # Decide whether to check for the regular or continuation prompt.
50 50 if current_block.contains(self._frontend._prompt_pos):
51 51 prompt = self._frontend._prompt
52 52 else:
53 53 prompt = self._frontend._continuation_prompt
54 54
55 55 # Only highlight if we can identify a prompt, but make sure not to
56 56 # highlight the prompt.
57 57 if string.startswith(prompt):
58 58 self._current_offset = len(prompt)
59 59 string = string[len(prompt):]
60 60 super(FrontendHighlighter, self).highlightBlock(string)
61 61
62 62 def rehighlightBlock(self, block):
63 63 """ Reimplemented to temporarily enable highlighting if disabled.
64 64 """
65 65 old = self.highlighting_on
66 66 self.highlighting_on = True
67 67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 68 self.highlighting_on = old
69 69
70 70 def setFormat(self, start, count, format):
71 71 """ Reimplemented to highlight selectively.
72 72 """
73 73 start += self._current_offset
74 74 super(FrontendHighlighter, self).setFormat(start, count, format)
75 75
76 76
77 77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 78 """ A Qt frontend for a generic Python kernel.
79 79 """
80 80
81 81 # The text to show when the kernel is (re)started.
82 82 banner = Unicode(config=True)
83 83
84 84 # An option and corresponding signal for overriding the default kernel
85 85 # interrupt behavior.
86 86 custom_interrupt = Bool(False)
87 87 custom_interrupt_requested = QtCore.Signal()
88 88
89 89 # An option and corresponding signals for overriding the default kernel
90 90 # restart behavior.
91 91 custom_restart = Bool(False)
92 92 custom_restart_kernel_died = QtCore.Signal(float)
93 93 custom_restart_requested = QtCore.Signal()
94 94
95 95 # Whether to automatically show calltips on open-parentheses.
96 96 enable_calltips = Bool(True, config=True,
97 97 help="Whether to draw information calltips on open-parentheses.")
98 98
99 99 clear_on_kernel_restart = Bool(True, config=True,
100 100 help="Whether to clear the console when the kernel is restarted")
101 101
102 102 confirm_restart = Bool(True, config=True,
103 103 help="Whether to ask for user confirmation when restarting kernel")
104 104
105 105 lexer_class = DottedObjectName(config=True,
106 106 help="The pygments lexer class to use."
107 107 )
108 108 def _lexer_class_changed(self, name, old, new):
109 109 lexer_class = import_item(new)
110 110 self.lexer = lexer_class()
111 111
112 112 def _lexer_class_default(self):
113 113 if py3compat.PY3:
114 114 return 'pygments.lexers.Python3Lexer'
115 115 else:
116 116 return 'pygments.lexers.PythonLexer'
117 117
118 118 lexer = Any()
119 119 def _lexer_default(self):
120 120 lexer_class = import_item(self.lexer_class)
121 121 return lexer_class()
122 122
123 123 # Emitted when a user visible 'execute_request' has been submitted to the
124 124 # kernel from the FrontendWidget. Contains the code to be executed.
125 125 executing = QtCore.Signal(object)
126 126
127 127 # Emitted when a user-visible 'execute_reply' has been received from the
128 128 # kernel and processed by the FrontendWidget. Contains the response message.
129 129 executed = QtCore.Signal(object)
130 130
131 131 # Emitted when an exit request has been received from the kernel.
132 132 exit_requested = QtCore.Signal(object)
133 133
134 134 # Protected class variables.
135 135 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
136 136 logical_line_transforms=[],
137 137 python_line_transforms=[],
138 138 )
139 139 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
140 140 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
141 141 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
142 142 _input_splitter_class = InputSplitter
143 143 _local_kernel = False
144 144 _highlighter = Instance(FrontendHighlighter)
145 145
146 146 #---------------------------------------------------------------------------
147 147 # 'object' interface
148 148 #---------------------------------------------------------------------------
149 149
150 150 def __init__(self, *args, **kw):
151 151 super(FrontendWidget, self).__init__(*args, **kw)
152 152 # FIXME: remove this when PySide min version is updated past 1.0.7
153 153 # forcefully disable calltips if PySide is < 1.0.7, because they crash
154 154 if qt.QT_API == qt.QT_API_PYSIDE:
155 155 import PySide
156 156 if PySide.__version_info__ < (1,0,7):
157 157 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
158 158 self.enable_calltips = False
159 159
160 160 # FrontendWidget protected variables.
161 161 self._bracket_matcher = BracketMatcher(self._control)
162 162 self._call_tip_widget = CallTipWidget(self._control)
163 163 self._completion_lexer = CompletionLexer(self.lexer)
164 164 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
165 165 self._hidden = False
166 166 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
167 167 self._input_splitter = self._input_splitter_class()
168 168 self._kernel_manager = None
169 169 self._kernel_client = None
170 170 self._request_info = {}
171 171 self._request_info['execute'] = {};
172 172 self._callback_dict = {}
173 173
174 174 # Configure the ConsoleWidget.
175 175 self.tab_width = 4
176 176 self._set_continuation_prompt('... ')
177 177
178 178 # Configure the CallTipWidget.
179 179 self._call_tip_widget.setFont(self.font)
180 180 self.font_changed.connect(self._call_tip_widget.setFont)
181 181
182 182 # Configure actions.
183 183 action = self._copy_raw_action
184 184 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
185 185 action.setEnabled(False)
186 186 action.setShortcut(QtGui.QKeySequence(key))
187 187 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
188 188 action.triggered.connect(self.copy_raw)
189 189 self.copy_available.connect(action.setEnabled)
190 190 self.addAction(action)
191 191
192 192 # Connect signal handlers.
193 193 document = self._control.document()
194 194 document.contentsChange.connect(self._document_contents_change)
195 195
196 196 # Set flag for whether we are connected via localhost.
197 197 self._local_kernel = kw.get('local_kernel',
198 198 FrontendWidget._local_kernel)
199 199
200 # Whether or not a clear_output call is pending new output.
201 self._pending_clearoutput = False
202
200 203 #---------------------------------------------------------------------------
201 204 # 'ConsoleWidget' public interface
202 205 #---------------------------------------------------------------------------
203 206
204 207 def copy(self):
205 208 """ Copy the currently selected text to the clipboard, removing prompts.
206 209 """
207 210 if self._page_control is not None and self._page_control.hasFocus():
208 211 self._page_control.copy()
209 212 elif self._control.hasFocus():
210 213 text = self._control.textCursor().selection().toPlainText()
211 214 if text:
212 215 text = self._prompt_transformer.transform_cell(text)
213 216 QtGui.QApplication.clipboard().setText(text)
214 217 else:
215 218 self.log.debug("frontend widget : unknown copy target")
216 219
217 220 #---------------------------------------------------------------------------
218 221 # 'ConsoleWidget' abstract interface
219 222 #---------------------------------------------------------------------------
220 223
221 224 def _is_complete(self, source, interactive):
222 225 """ Returns whether 'source' can be completely processed and a new
223 226 prompt created. When triggered by an Enter/Return key press,
224 227 'interactive' is True; otherwise, it is False.
225 228 """
226 229 self._input_splitter.reset()
227 230 try:
228 231 complete = self._input_splitter.push(source)
229 232 except SyntaxError:
230 233 return True
231 234 if interactive:
232 235 complete = not self._input_splitter.push_accepts_more()
233 236 return complete
234 237
235 238 def _execute(self, source, hidden):
236 239 """ Execute 'source'. If 'hidden', do not show any output.
237 240
238 241 See parent class :meth:`execute` docstring for full details.
239 242 """
240 243 msg_id = self.kernel_client.execute(source, hidden)
241 244 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
242 245 self._hidden = hidden
243 246 if not hidden:
244 247 self.executing.emit(source)
245 248
246 249 def _prompt_started_hook(self):
247 250 """ Called immediately after a new prompt is displayed.
248 251 """
249 252 if not self._reading:
250 253 self._highlighter.highlighting_on = True
251 254
252 255 def _prompt_finished_hook(self):
253 256 """ Called immediately after a prompt is finished, i.e. when some input
254 257 will be processed and a new prompt displayed.
255 258 """
256 259 # Flush all state from the input splitter so the next round of
257 260 # reading input starts with a clean buffer.
258 261 self._input_splitter.reset()
259 262
260 263 if not self._reading:
261 264 self._highlighter.highlighting_on = False
262 265
263 266 def _tab_pressed(self):
264 267 """ Called when the tab key is pressed. Returns whether to continue
265 268 processing the event.
266 269 """
267 270 # Perform tab completion if:
268 271 # 1) The cursor is in the input buffer.
269 272 # 2) There is a non-whitespace character before the cursor.
270 273 text = self._get_input_buffer_cursor_line()
271 274 if text is None:
272 275 return False
273 276 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
274 277 if complete:
275 278 self._complete()
276 279 return not complete
277 280
278 281 #---------------------------------------------------------------------------
279 282 # 'ConsoleWidget' protected interface
280 283 #---------------------------------------------------------------------------
281 284
282 285 def _context_menu_make(self, pos):
283 286 """ Reimplemented to add an action for raw copy.
284 287 """
285 288 menu = super(FrontendWidget, self)._context_menu_make(pos)
286 289 for before_action in menu.actions():
287 290 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
288 291 QtGui.QKeySequence.ExactMatch:
289 292 menu.insertAction(before_action, self._copy_raw_action)
290 293 break
291 294 return menu
292 295
293 296 def request_interrupt_kernel(self):
294 297 if self._executing:
295 298 self.interrupt_kernel()
296 299
297 300 def request_restart_kernel(self):
298 301 message = 'Are you sure you want to restart the kernel?'
299 302 self.restart_kernel(message, now=False)
300 303
301 304 def _event_filter_console_keypress(self, event):
302 305 """ Reimplemented for execution interruption and smart backspace.
303 306 """
304 307 key = event.key()
305 308 if self._control_key_down(event.modifiers(), include_command=False):
306 309
307 310 if key == QtCore.Qt.Key_C and self._executing:
308 311 self.request_interrupt_kernel()
309 312 return True
310 313
311 314 elif key == QtCore.Qt.Key_Period:
312 315 self.request_restart_kernel()
313 316 return True
314 317
315 318 elif not event.modifiers() & QtCore.Qt.AltModifier:
316 319
317 320 # Smart backspace: remove four characters in one backspace if:
318 321 # 1) everything left of the cursor is whitespace
319 322 # 2) the four characters immediately left of the cursor are spaces
320 323 if key == QtCore.Qt.Key_Backspace:
321 324 col = self._get_input_buffer_cursor_column()
322 325 cursor = self._control.textCursor()
323 326 if col > 3 and not cursor.hasSelection():
324 327 text = self._get_input_buffer_cursor_line()[:col]
325 328 if text.endswith(' ') and not text.strip():
326 329 cursor.movePosition(QtGui.QTextCursor.Left,
327 330 QtGui.QTextCursor.KeepAnchor, 4)
328 331 cursor.removeSelectedText()
329 332 return True
330 333
331 334 return super(FrontendWidget, self)._event_filter_console_keypress(event)
332 335
333 336 def _insert_continuation_prompt(self, cursor):
334 337 """ Reimplemented for auto-indentation.
335 338 """
336 339 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
337 340 cursor.insertText(' ' * self._input_splitter.indent_spaces)
338 341
339 342 #---------------------------------------------------------------------------
340 343 # 'BaseFrontendMixin' abstract interface
341 344 #---------------------------------------------------------------------------
345 def _handle_clear_output(self, msg):
346 """Handle clear output messages."""
347 if not self._hidden and self._is_from_this_session(msg):
348 wait = msg['content'].get('wait', True)
349 if wait:
350 self._pending_clearoutput = True
351 else:
352 self.clear_output()
342 353
343 354 def _handle_complete_reply(self, rep):
344 355 """ Handle replies for tab completion.
345 356 """
346 357 self.log.debug("complete: %s", rep.get('content', ''))
347 358 cursor = self._get_cursor()
348 359 info = self._request_info.get('complete')
349 360 if info and info.id == rep['parent_header']['msg_id'] and \
350 361 info.pos == cursor.position():
351 362 text = '.'.join(self._get_context())
352 363 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
353 364 self._complete_with_items(cursor, rep['content']['matches'])
354 365
355 366 def _silent_exec_callback(self, expr, callback):
356 367 """Silently execute `expr` in the kernel and call `callback` with reply
357 368
358 369 the `expr` is evaluated silently in the kernel (without) output in
359 370 the frontend. Call `callback` with the
360 371 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
361 372
362 373 Parameters
363 374 ----------
364 375 expr : string
365 376 valid string to be executed by the kernel.
366 377 callback : function
367 378 function accepting one argument, as a string. The string will be
368 379 the `repr` of the result of evaluating `expr`
369 380
370 381 The `callback` is called with the `repr()` of the result of `expr` as
371 382 first argument. To get the object, do `eval()` on the passed value.
372 383
373 384 See Also
374 385 --------
375 386 _handle_exec_callback : private method, deal with calling callback with reply
376 387
377 388 """
378 389
379 390 # generate uuid, which would be used as an indication of whether or
380 391 # not the unique request originated from here (can use msg id ?)
381 392 local_uuid = str(uuid.uuid1())
382 393 msg_id = self.kernel_client.execute('',
383 394 silent=True, user_expressions={ local_uuid:expr })
384 395 self._callback_dict[local_uuid] = callback
385 396 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
386 397
387 398 def _handle_exec_callback(self, msg):
388 399 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
389 400
390 401 Parameters
391 402 ----------
392 403 msg : raw message send by the kernel containing an `user_expressions`
393 404 and having a 'silent_exec_callback' kind.
394 405
395 406 Notes
396 407 -----
397 408 This function will look for a `callback` associated with the
398 409 corresponding message id. Association has been made by
399 410 `_silent_exec_callback`. `callback` is then called with the `repr()`
400 411 of the value of corresponding `user_expressions` as argument.
401 412 `callback` is then removed from the known list so that any message
402 413 coming again with the same id won't trigger it.
403 414
404 415 """
405 416
406 417 user_exp = msg['content'].get('user_expressions')
407 418 if not user_exp:
408 419 return
409 420 for expression in user_exp:
410 421 if expression in self._callback_dict:
411 422 self._callback_dict.pop(expression)(user_exp[expression])
412 423
413 424 def _handle_execute_reply(self, msg):
414 425 """ Handles replies for code execution.
415 426 """
416 427 self.log.debug("execute: %s", msg.get('content', ''))
417 428 msg_id = msg['parent_header']['msg_id']
418 429 info = self._request_info['execute'].get(msg_id)
419 430 # unset reading flag, because if execute finished, raw_input can't
420 431 # still be pending.
421 432 self._reading = False
422 433 if info and info.kind == 'user' and not self._hidden:
423 434 # Make sure that all output from the SUB channel has been processed
424 435 # before writing a new prompt.
425 436 self.kernel_client.iopub_channel.flush()
426 437
427 438 # Reset the ANSI style information to prevent bad text in stdout
428 439 # from messing up our colors. We're not a true terminal so we're
429 440 # allowed to do this.
430 441 if self.ansi_codes:
431 442 self._ansi_processor.reset_sgr()
432 443
433 444 content = msg['content']
434 445 status = content['status']
435 446 if status == 'ok':
436 447 self._process_execute_ok(msg)
437 448 elif status == 'error':
438 449 self._process_execute_error(msg)
439 450 elif status == 'aborted':
440 451 self._process_execute_abort(msg)
441 452
442 453 self._show_interpreter_prompt_for_reply(msg)
443 454 self.executed.emit(msg)
444 455 self._request_info['execute'].pop(msg_id)
445 456 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
446 457 self._handle_exec_callback(msg)
447 458 self._request_info['execute'].pop(msg_id)
448 459 else:
449 460 super(FrontendWidget, self)._handle_execute_reply(msg)
450 461
451 462 def _handle_input_request(self, msg):
452 463 """ Handle requests for raw_input.
453 464 """
454 465 self.log.debug("input: %s", msg.get('content', ''))
455 466 if self._hidden:
456 467 raise RuntimeError('Request for raw input during hidden execution.')
457 468
458 469 # Make sure that all output from the SUB channel has been processed
459 470 # before entering readline mode.
460 471 self.kernel_client.iopub_channel.flush()
461 472
462 473 def callback(line):
463 474 self.kernel_client.stdin_channel.input(line)
464 475 if self._reading:
465 476 self.log.debug("Got second input request, assuming first was interrupted.")
466 477 self._reading = False
467 478 self._readline(msg['content']['prompt'], callback=callback)
468 479
469 480 def _kernel_restarted_message(self, died=True):
470 481 msg = "Kernel died, restarting" if died else "Kernel restarting"
471 482 self._append_html("<br>%s<hr><br>" % msg,
472 483 before_prompt=False
473 484 )
474 485
475 486 def _handle_kernel_died(self, since_last_heartbeat):
476 487 """Handle the kernel's death (if we do not own the kernel).
477 488 """
478 489 self.log.warn("kernel died: %s", since_last_heartbeat)
479 490 if self.custom_restart:
480 491 self.custom_restart_kernel_died.emit(since_last_heartbeat)
481 492 else:
482 493 self._kernel_restarted_message(died=True)
483 494 self.reset()
484 495
485 496 def _handle_kernel_restarted(self, died=True):
486 497 """Notice that the autorestarter restarted the kernel.
487 498
488 499 There's nothing to do but show a message.
489 500 """
490 501 self.log.warn("kernel restarted")
491 502 self._kernel_restarted_message(died=died)
492 503 self.reset()
493 504
494 505 def _handle_object_info_reply(self, rep):
495 506 """ Handle replies for call tips.
496 507 """
497 508 self.log.debug("oinfo: %s", rep.get('content', ''))
498 509 cursor = self._get_cursor()
499 510 info = self._request_info.get('call_tip')
500 511 if info and info.id == rep['parent_header']['msg_id'] and \
501 512 info.pos == cursor.position():
502 513 # Get the information for a call tip. For now we format the call
503 514 # line as string, later we can pass False to format_call and
504 515 # syntax-highlight it ourselves for nicer formatting in the
505 516 # calltip.
506 517 content = rep['content']
507 518 # if this is from pykernel, 'docstring' will be the only key
508 519 if content.get('ismagic', False):
509 520 # Don't generate a call-tip for magics. Ideally, we should
510 521 # generate a tooltip, but not on ( like we do for actual
511 522 # callables.
512 523 call_info, doc = None, None
513 524 else:
514 525 call_info, doc = call_tip(content, format_call=True)
515 526 if call_info or doc:
516 527 self._call_tip_widget.show_call_info(call_info, doc)
517 528
518 529 def _handle_pyout(self, msg):
519 530 """ Handle display hook output.
520 531 """
521 532 self.log.debug("pyout: %s", msg.get('content', ''))
522 533 if not self._hidden and self._is_from_this_session(msg):
534 self.flush_clearoutput()
523 535 text = msg['content']['data']
524 536 self._append_plain_text(text + '\n', before_prompt=True)
525 537
526 538 def _handle_stream(self, msg):
527 539 """ Handle stdout, stderr, and stdin.
528 540 """
529 541 self.log.debug("stream: %s", msg.get('content', ''))
530 542 if not self._hidden and self._is_from_this_session(msg):
531 # Most consoles treat tabs as being 8 space characters. Convert tabs
532 # to spaces so that output looks as expected regardless of this
533 # widget's tab width.
534 text = msg['content']['data'].expandtabs(8)
535
536 self._append_plain_text(text, before_prompt=True)
537 self._control.moveCursor(QtGui.QTextCursor.End)
543 self.flush_clearoutput()
544 self.append_stream(msg['content']['data'])
538 545
539 546 def _handle_shutdown_reply(self, msg):
540 547 """ Handle shutdown signal, only if from other console.
541 548 """
542 549 self.log.warn("shutdown: %s", msg.get('content', ''))
543 550 restart = msg.get('content', {}).get('restart', False)
544 551 if not self._hidden and not self._is_from_this_session(msg):
545 552 # got shutdown reply, request came from session other than ours
546 553 if restart:
547 554 # someone restarted the kernel, handle it
548 555 self._handle_kernel_restarted(died=False)
549 556 else:
550 557 # kernel was shutdown permanently
551 558 # this triggers exit_requested if the kernel was local,
552 559 # and a dialog if the kernel was remote,
553 560 # so we don't suddenly clear the qtconsole without asking.
554 561 if self._local_kernel:
555 562 self.exit_requested.emit(self)
556 563 else:
557 564 title = self.window().windowTitle()
558 565 reply = QtGui.QMessageBox.question(self, title,
559 566 "Kernel has been shutdown permanently. "
560 567 "Close the Console?",
561 568 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
562 569 if reply == QtGui.QMessageBox.Yes:
563 570 self.exit_requested.emit(self)
564 571
565 572 def _handle_status(self, msg):
566 573 """Handle status message"""
567 574 # This is where a busy/idle indicator would be triggered,
568 575 # when we make one.
569 576 state = msg['content'].get('execution_state', '')
570 577 if state == 'starting':
571 578 # kernel started while we were running
572 579 if self._executing:
573 580 self._handle_kernel_restarted(died=True)
574 581 elif state == 'idle':
575 582 pass
576 583 elif state == 'busy':
577 584 pass
578 585
579 586 def _started_channels(self):
580 587 """ Called when the KernelManager channels have started listening or
581 588 when the frontend is assigned an already listening KernelManager.
582 589 """
583 590 self.reset(clear=True)
584 591
585 592 #---------------------------------------------------------------------------
586 593 # 'FrontendWidget' public interface
587 594 #---------------------------------------------------------------------------
588 595
589 596 def copy_raw(self):
590 597 """ Copy the currently selected text to the clipboard without attempting
591 598 to remove prompts or otherwise alter the text.
592 599 """
593 600 self._control.copy()
594 601
595 602 def execute_file(self, path, hidden=False):
596 603 """ Attempts to execute file with 'path'. If 'hidden', no output is
597 604 shown.
598 605 """
599 606 self.execute('execfile(%r)' % path, hidden=hidden)
600 607
601 608 def interrupt_kernel(self):
602 609 """ Attempts to interrupt the running kernel.
603 610
604 611 Also unsets _reading flag, to avoid runtime errors
605 612 if raw_input is called again.
606 613 """
607 614 if self.custom_interrupt:
608 615 self._reading = False
609 616 self.custom_interrupt_requested.emit()
610 617 elif self.kernel_manager:
611 618 self._reading = False
612 619 self.kernel_manager.interrupt_kernel()
613 620 else:
614 621 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
615 622
616 623 def reset(self, clear=False):
617 624 """ Resets the widget to its initial state if ``clear`` parameter
618 625 is True, otherwise
619 626 prints a visual indication of the fact that the kernel restarted, but
620 627 does not clear the traces from previous usage of the kernel before it
621 628 was restarted. With ``clear=True``, it is similar to ``%clear``, but
622 629 also re-writes the banner and aborts execution if necessary.
623 630 """
624 631 if self._executing:
625 632 self._executing = False
626 633 self._request_info['execute'] = {}
627 634 self._reading = False
628 635 self._highlighter.highlighting_on = False
629 636
630 637 if clear:
631 638 self._control.clear()
632 639 self._append_plain_text(self.banner)
633 640 # update output marker for stdout/stderr, so that startup
634 641 # messages appear after banner:
635 642 self._append_before_prompt_pos = self._get_cursor().position()
636 643 self._show_interpreter_prompt()
637 644
638 645 def restart_kernel(self, message, now=False):
639 646 """ Attempts to restart the running kernel.
640 647 """
641 648 # FIXME: now should be configurable via a checkbox in the dialog. Right
642 649 # now at least the heartbeat path sets it to True and the manual restart
643 650 # to False. But those should just be the pre-selected states of a
644 651 # checkbox that the user could override if so desired. But I don't know
645 652 # enough Qt to go implementing the checkbox now.
646 653
647 654 if self.custom_restart:
648 655 self.custom_restart_requested.emit()
649 656 return
650 657
651 658 if self.kernel_manager:
652 659 # Pause the heart beat channel to prevent further warnings.
653 660 self.kernel_client.hb_channel.pause()
654 661
655 662 # Prompt the user to restart the kernel. Un-pause the heartbeat if
656 663 # they decline. (If they accept, the heartbeat will be un-paused
657 664 # automatically when the kernel is restarted.)
658 665 if self.confirm_restart:
659 666 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
660 667 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
661 668 message, buttons)
662 669 do_restart = result == QtGui.QMessageBox.Yes
663 670 else:
664 671 # confirm_restart is False, so we don't need to ask user
665 672 # anything, just do the restart
666 673 do_restart = True
667 674 if do_restart:
668 675 try:
669 676 self.kernel_manager.restart_kernel(now=now)
670 677 except RuntimeError as e:
671 678 self._append_plain_text(
672 679 'Error restarting kernel: %s\n' % e,
673 680 before_prompt=True
674 681 )
675 682 else:
676 683 self._append_html("<br>Restarting kernel...\n<hr><br>",
677 684 before_prompt=True,
678 685 )
679 686 else:
680 687 self.kernel_client.hb_channel.unpause()
681 688
682 689 else:
683 690 self._append_plain_text(
684 691 'Cannot restart a Kernel I did not start\n',
685 692 before_prompt=True
686 693 )
687 694
695 def append_stream(self, text):
696 """Appends text to the output stream."""
697 # Most consoles treat tabs as being 8 space characters. Convert tabs
698 # to spaces so that output looks as expected regardless of this
699 # widget's tab width.
700 text = text.expandtabs(8)
701
702 print([ord(c) for c in text])
703 self._append_plain_text(text, before_prompt=True)
704 self._control.moveCursor(QtGui.QTextCursor.End)
705
706 def flush_clearoutput(self):
707 """If a clearoutput is pending, execute it."""
708 if self._pending_clearoutput:
709 self._pending_clearoutput = False
710 self.clear_output()
711
712 def clear_output(self):
713 """Clear the output area."""
714 cursor = self._control.textCursor()
715 cursor.beginEditBlock()
716 cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
717 cursor.insertText('')
718 cursor.endEditBlock()
719
688 720 #---------------------------------------------------------------------------
689 721 # 'FrontendWidget' protected interface
690 722 #---------------------------------------------------------------------------
691 723
692 724 def _call_tip(self):
693 725 """ Shows a call tip, if appropriate, at the current cursor location.
694 726 """
695 727 # Decide if it makes sense to show a call tip
696 728 if not self.enable_calltips:
697 729 return False
698 730 cursor = self._get_cursor()
699 731 cursor.movePosition(QtGui.QTextCursor.Left)
700 732 if cursor.document().characterAt(cursor.position()) != '(':
701 733 return False
702 734 context = self._get_context(cursor)
703 735 if not context:
704 736 return False
705 737
706 738 # Send the metadata request to the kernel
707 739 name = '.'.join(context)
708 740 msg_id = self.kernel_client.object_info(name)
709 741 pos = self._get_cursor().position()
710 742 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
711 743 return True
712 744
713 745 def _complete(self):
714 746 """ Performs completion at the current cursor location.
715 747 """
716 748 context = self._get_context()
717 749 if context:
718 750 # Send the completion request to the kernel
719 751 msg_id = self.kernel_client.complete(
720 752 '.'.join(context), # text
721 753 self._get_input_buffer_cursor_line(), # line
722 754 self._get_input_buffer_cursor_column(), # cursor_pos
723 755 self.input_buffer) # block
724 756 pos = self._get_cursor().position()
725 757 info = self._CompletionRequest(msg_id, pos)
726 758 self._request_info['complete'] = info
727 759
728 760 def _get_context(self, cursor=None):
729 761 """ Gets the context for the specified cursor (or the current cursor
730 762 if none is specified).
731 763 """
732 764 if cursor is None:
733 765 cursor = self._get_cursor()
734 766 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
735 767 QtGui.QTextCursor.KeepAnchor)
736 768 text = cursor.selection().toPlainText()
737 769 return self._completion_lexer.get_context(text)
738 770
739 771 def _process_execute_abort(self, msg):
740 772 """ Process a reply for an aborted execution request.
741 773 """
742 774 self._append_plain_text("ERROR: execution aborted\n")
743 775
744 776 def _process_execute_error(self, msg):
745 777 """ Process a reply for an execution request that resulted in an error.
746 778 """
747 779 content = msg['content']
748 780 # If a SystemExit is passed along, this means exit() was called - also
749 781 # all the ipython %exit magic syntax of '-k' to be used to keep
750 782 # the kernel running
751 783 if content['ename']=='SystemExit':
752 784 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
753 785 self._keep_kernel_on_exit = keepkernel
754 786 self.exit_requested.emit(self)
755 787 else:
756 788 traceback = ''.join(content['traceback'])
757 789 self._append_plain_text(traceback)
758 790
759 791 def _process_execute_ok(self, msg):
760 792 """ Process a reply for a successful execution request.
761 793 """
762 794 payload = msg['content']['payload']
763 795 for item in payload:
764 796 if not self._process_execute_payload(item):
765 797 warning = 'Warning: received unknown payload of type %s'
766 798 print(warning % repr(item['source']))
767 799
768 800 def _process_execute_payload(self, item):
769 801 """ Process a single payload item from the list of payload items in an
770 802 execution reply. Returns whether the payload was handled.
771 803 """
772 804 # The basic FrontendWidget doesn't handle payloads, as they are a
773 805 # mechanism for going beyond the standard Python interpreter model.
774 806 return False
775 807
776 808 def _show_interpreter_prompt(self):
777 809 """ Shows a prompt for the interpreter.
778 810 """
779 811 self._show_prompt('>>> ')
780 812
781 813 def _show_interpreter_prompt_for_reply(self, msg):
782 814 """ Shows a prompt for the interpreter given an 'execute_reply' message.
783 815 """
784 816 self._show_interpreter_prompt()
785 817
786 818 #------ Signal handlers ----------------------------------------------------
787 819
788 820 def _document_contents_change(self, position, removed, added):
789 821 """ Called whenever the document's content changes. Display a call tip
790 822 if appropriate.
791 823 """
792 824 # Calculate where the cursor should be *after* the change:
793 825 position += added
794 826
795 827 document = self._control.document()
796 828 if position == self._get_cursor().position():
797 829 self._call_tip()
798 830
799 831 #------ Trait default initializers -----------------------------------------
800 832
801 833 def _banner_default(self):
802 834 """ Returns the standard Python banner.
803 835 """
804 836 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
805 837 '"license" for more information.'
806 838 return banner % (sys.version, sys.platform)
@@ -1,602 +1,603 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
144 143 def _handle_complete_reply(self, rep):
145 144 """ Reimplemented to support IPython's improved completion machinery.
146 145 """
147 146 self.log.debug("complete: %s", rep.get('content', ''))
148 147 cursor = self._get_cursor()
149 148 info = self._request_info.get('complete')
150 149 if info and info.id == rep['parent_header']['msg_id'] and \
151 150 info.pos == cursor.position():
152 151 matches = rep['content']['matches']
153 152 text = rep['content']['matched_text']
154 153 offset = len(text)
155 154
156 155 # Clean up matches with period and path separators if the matched
157 156 # text has not been transformed. This is done by truncating all
158 157 # but the last component and then suitably decreasing the offset
159 158 # between the current cursor position and the start of completion.
160 159 if len(matches) > 1 and matches[0][:offset] == text:
161 160 parts = re.split(r'[./\\]', text)
162 161 sep_count = len(parts) - 1
163 162 if sep_count:
164 163 chop_length = sum(map(len, parts[:sep_count])) + sep_count
165 164 matches = [ match[chop_length:] for match in matches ]
166 165 offset -= chop_length
167 166
168 167 # Move the cursor to the start of the match and complete.
169 168 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
170 169 self._complete_with_items(cursor, matches)
171 170
172 171 def _handle_execute_reply(self, msg):
173 172 """ Reimplemented to support prompt requests.
174 173 """
175 174 msg_id = msg['parent_header'].get('msg_id')
176 175 info = self._request_info['execute'].get(msg_id)
177 176 if info and info.kind == 'prompt':
178 177 content = msg['content']
179 178 if content['status'] == 'aborted':
180 179 self._show_interpreter_prompt()
181 180 else:
182 181 number = content['execution_count'] + 1
183 182 self._show_interpreter_prompt(number)
184 183 self._request_info['execute'].pop(msg_id)
185 184 else:
186 185 super(IPythonWidget, self)._handle_execute_reply(msg)
187 186
188 187 def _handle_history_reply(self, msg):
189 188 """ Implemented to handle history tail replies, which are only supported
190 189 by the IPython kernel.
191 190 """
192 191 content = msg['content']
193 192 if 'history' not in content:
194 193 self.log.error("History request failed: %r"%content)
195 194 if content.get('status', '') == 'aborted' and \
196 195 not self._retrying_history_request:
197 196 # a *different* action caused this request to be aborted, so
198 197 # we should try again.
199 198 self.log.error("Retrying aborted history request")
200 199 # prevent multiple retries of aborted requests:
201 200 self._retrying_history_request = True
202 201 # wait out the kernel's queue flush, which is currently timed at 0.1s
203 202 time.sleep(0.25)
204 203 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
205 204 else:
206 205 self._retrying_history_request = False
207 206 return
208 207 # reset retry flag
209 208 self._retrying_history_request = False
210 209 history_items = content['history']
211 210 self.log.debug("Received history reply with %i entries", len(history_items))
212 211 items = []
213 212 last_cell = u""
214 213 for _, _, cell in history_items:
215 214 cell = cell.rstrip()
216 215 if cell != last_cell:
217 216 items.append(cell)
218 217 last_cell = cell
219 218 self._set_history(items)
220 219
221 220 def _handle_pyout(self, msg):
222 221 """ Reimplemented for IPython-style "display hook".
223 222 """
224 223 self.log.debug("pyout: %s", msg.get('content', ''))
225 224 if not self._hidden and self._is_from_this_session(msg):
225 self.flush_clearoutput()
226 226 content = msg['content']
227 227 prompt_number = content.get('execution_count', 0)
228 228 data = content['data']
229 229 if 'text/html' in data:
230 230 self._append_plain_text(self.output_sep, True)
231 231 self._append_html(self._make_out_prompt(prompt_number), True)
232 232 html = data['text/html']
233 233 self._append_plain_text('\n', True)
234 234 self._append_html(html + self.output_sep2, True)
235 235 elif 'text/plain' in data:
236 236 self._append_plain_text(self.output_sep, True)
237 237 self._append_html(self._make_out_prompt(prompt_number), True)
238 238 text = data['text/plain']
239 239 # If the repr is multiline, make sure we start on a new line,
240 240 # so that its lines are aligned.
241 241 if "\n" in text and not self.output_sep.endswith("\n"):
242 242 self._append_plain_text('\n', True)
243 243 self._append_plain_text(text + self.output_sep2, True)
244 244
245 245 def _handle_display_data(self, msg):
246 246 """ The base handler for the ``display_data`` message.
247 247 """
248 248 self.log.debug("display: %s", msg.get('content', ''))
249 249 # For now, we don't display data from other frontends, but we
250 250 # eventually will as this allows all frontends to monitor the display
251 251 # data. But we need to figure out how to handle this in the GUI.
252 252 if not self._hidden and self._is_from_this_session(msg):
253 self.flush_clearoutput()
253 254 source = msg['content']['source']
254 255 data = msg['content']['data']
255 256 metadata = msg['content']['metadata']
256 257 # In the regular IPythonWidget, we simply print the plain text
257 258 # representation.
258 259 if 'text/html' in data:
259 260 html = data['text/html']
260 261 self._append_html(html, True)
261 262 elif 'text/plain' in data:
262 263 text = data['text/plain']
263 264 self._append_plain_text(text, True)
264 265 # This newline seems to be needed for text and html output.
265 266 self._append_plain_text(u'\n', True)
266 267
267 268 def _handle_kernel_info_reply(self, rep):
268 269 """ Handle kernel info replies.
269 270 """
270 271 if not self._guiref_loaded:
271 272 if rep['content'].get('language') == 'python':
272 273 self._load_guiref_magic()
273 274 self._guiref_loaded = True
274 275
275 276 def _started_channels(self):
276 277 """Reimplemented to make a history request and load %guiref."""
277 278 super(IPythonWidget, self)._started_channels()
278 279
279 280 # The reply will trigger %guiref load provided language=='python'
280 281 self.kernel_client.kernel_info()
281 282
282 283 self.kernel_client.shell_channel.history(hist_access_type='tail',
283 284 n=1000)
284 285
285 286 def _started_kernel(self):
286 287 """Load %guiref when the kernel starts (if channels are also started).
287 288
288 289 Principally triggered by kernel restart.
289 290 """
290 291 if self.kernel_client.shell_channel is not None:
291 292 self._load_guiref_magic()
292 293
293 294 def _load_guiref_magic(self):
294 295 """Load %guiref magic."""
295 296 self.kernel_client.shell_channel.execute('\n'.join([
296 297 "try:",
297 298 " _usage",
298 299 "except:",
299 300 " from IPython.core import usage as _usage",
300 301 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
301 302 " del _usage",
302 303 ]), silent=True)
303 304
304 305 #---------------------------------------------------------------------------
305 306 # 'ConsoleWidget' public interface
306 307 #---------------------------------------------------------------------------
307 308
308 309 #---------------------------------------------------------------------------
309 310 # 'FrontendWidget' public interface
310 311 #---------------------------------------------------------------------------
311 312
312 313 def execute_file(self, path, hidden=False):
313 314 """ Reimplemented to use the 'run' magic.
314 315 """
315 316 # Use forward slashes on Windows to avoid escaping each separator.
316 317 if sys.platform == 'win32':
317 318 path = os.path.normpath(path).replace('\\', '/')
318 319
319 320 # Perhaps we should not be using %run directly, but while we
320 321 # are, it is necessary to quote or escape filenames containing spaces
321 322 # or quotes.
322 323
323 324 # In earlier code here, to minimize escaping, we sometimes quoted the
324 325 # filename with single quotes. But to do this, this code must be
325 326 # platform-aware, because run uses shlex rather than python string
326 327 # parsing, so that:
327 328 # * In Win: single quotes can be used in the filename without quoting,
328 329 # and we cannot use single quotes to quote the filename.
329 330 # * In *nix: we can escape double quotes in a double quoted filename,
330 331 # but can't escape single quotes in a single quoted filename.
331 332
332 333 # So to keep this code non-platform-specific and simple, we now only
333 334 # use double quotes to quote filenames, and escape when needed:
334 335 if ' ' in path or "'" in path or '"' in path:
335 336 path = '"%s"' % path.replace('"', '\\"')
336 337 self.execute('%%run %s' % path, hidden=hidden)
337 338
338 339 #---------------------------------------------------------------------------
339 340 # 'FrontendWidget' protected interface
340 341 #---------------------------------------------------------------------------
341 342
342 343 def _complete(self):
343 344 """ Reimplemented to support IPython's improved completion machinery.
344 345 """
345 346 # We let the kernel split the input line, so we *always* send an empty
346 347 # text field. Readline-based frontends do get a real text field which
347 348 # they can use.
348 349 text = ''
349 350
350 351 # Send the completion request to the kernel
351 352 msg_id = self.kernel_client.shell_channel.complete(
352 353 text, # text
353 354 self._get_input_buffer_cursor_line(), # line
354 355 self._get_input_buffer_cursor_column(), # cursor_pos
355 356 self.input_buffer) # block
356 357 pos = self._get_cursor().position()
357 358 info = self._CompletionRequest(msg_id, pos)
358 359 self._request_info['complete'] = info
359 360
360 361 def _process_execute_error(self, msg):
361 362 """ Reimplemented for IPython-style traceback formatting.
362 363 """
363 364 content = msg['content']
364 365 traceback = '\n'.join(content['traceback']) + '\n'
365 366 if False:
366 367 # FIXME: For now, tracebacks come as plain text, so we can't use
367 368 # the html renderer yet. Once we refactor ultratb to produce
368 369 # properly styled tracebacks, this branch should be the default
369 370 traceback = traceback.replace(' ', '&nbsp;')
370 371 traceback = traceback.replace('\n', '<br/>')
371 372
372 373 ename = content['ename']
373 374 ename_styled = '<span class="error">%s</span>' % ename
374 375 traceback = traceback.replace(ename, ename_styled)
375 376
376 377 self._append_html(traceback)
377 378 else:
378 379 # This is the fallback for now, using plain text with ansi escapes
379 380 self._append_plain_text(traceback)
380 381
381 382 def _process_execute_payload(self, item):
382 383 """ Reimplemented to dispatch payloads to handler methods.
383 384 """
384 385 handler = self._payload_handlers.get(item['source'])
385 386 if handler is None:
386 387 # We have no handler for this type of payload, simply ignore it
387 388 return False
388 389 else:
389 390 handler(item)
390 391 return True
391 392
392 393 def _show_interpreter_prompt(self, number=None):
393 394 """ Reimplemented for IPython-style prompts.
394 395 """
395 396 # If a number was not specified, make a prompt number request.
396 397 if number is None:
397 398 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
398 399 info = self._ExecutionRequest(msg_id, 'prompt')
399 400 self._request_info['execute'][msg_id] = info
400 401 return
401 402
402 403 # Show a new prompt and save information about it so that it can be
403 404 # updated later if the prompt number turns out to be wrong.
404 405 self._prompt_sep = self.input_sep
405 406 self._show_prompt(self._make_in_prompt(number), html=True)
406 407 block = self._control.document().lastBlock()
407 408 length = len(self._prompt)
408 409 self._previous_prompt_obj = self._PromptBlock(block, length, number)
409 410
410 411 # Update continuation prompt to reflect (possibly) new prompt length.
411 412 self._set_continuation_prompt(
412 413 self._make_continuation_prompt(self._prompt), html=True)
413 414
414 415 def _show_interpreter_prompt_for_reply(self, msg):
415 416 """ Reimplemented for IPython-style prompts.
416 417 """
417 418 # Update the old prompt number if necessary.
418 419 content = msg['content']
419 420 # abort replies do not have any keys:
420 421 if content['status'] == 'aborted':
421 422 if self._previous_prompt_obj:
422 423 previous_prompt_number = self._previous_prompt_obj.number
423 424 else:
424 425 previous_prompt_number = 0
425 426 else:
426 427 previous_prompt_number = content['execution_count']
427 428 if self._previous_prompt_obj and \
428 429 self._previous_prompt_obj.number != previous_prompt_number:
429 430 block = self._previous_prompt_obj.block
430 431
431 432 # Make sure the prompt block has not been erased.
432 433 if block.isValid() and block.text():
433 434
434 435 # Remove the old prompt and insert a new prompt.
435 436 cursor = QtGui.QTextCursor(block)
436 437 cursor.movePosition(QtGui.QTextCursor.Right,
437 438 QtGui.QTextCursor.KeepAnchor,
438 439 self._previous_prompt_obj.length)
439 440 prompt = self._make_in_prompt(previous_prompt_number)
440 441 self._prompt = self._insert_html_fetching_plain_text(
441 442 cursor, prompt)
442 443
443 444 # When the HTML is inserted, Qt blows away the syntax
444 445 # highlighting for the line, so we need to rehighlight it.
445 446 self._highlighter.rehighlightBlock(cursor.block())
446 447
447 448 self._previous_prompt_obj = None
448 449
449 450 # Show a new prompt with the kernel's estimated prompt number.
450 451 self._show_interpreter_prompt(previous_prompt_number + 1)
451 452
452 453 #---------------------------------------------------------------------------
453 454 # 'IPythonWidget' interface
454 455 #---------------------------------------------------------------------------
455 456
456 457 def set_default_style(self, colors='lightbg'):
457 458 """ Sets the widget style to the class defaults.
458 459
459 460 Parameters
460 461 ----------
461 462 colors : str, optional (default lightbg)
462 463 Whether to use the default IPython light background or dark
463 464 background or B&W style.
464 465 """
465 466 colors = colors.lower()
466 467 if colors=='lightbg':
467 468 self.style_sheet = styles.default_light_style_sheet
468 469 self.syntax_style = styles.default_light_syntax_style
469 470 elif colors=='linux':
470 471 self.style_sheet = styles.default_dark_style_sheet
471 472 self.syntax_style = styles.default_dark_syntax_style
472 473 elif colors=='nocolor':
473 474 self.style_sheet = styles.default_bw_style_sheet
474 475 self.syntax_style = styles.default_bw_syntax_style
475 476 else:
476 477 raise KeyError("No such color scheme: %s"%colors)
477 478
478 479 #---------------------------------------------------------------------------
479 480 # 'IPythonWidget' protected interface
480 481 #---------------------------------------------------------------------------
481 482
482 483 def _edit(self, filename, line=None):
483 484 """ Opens a Python script for editing.
484 485
485 486 Parameters
486 487 ----------
487 488 filename : str
488 489 A path to a local system file.
489 490
490 491 line : int, optional
491 492 A line of interest in the file.
492 493 """
493 494 if self.custom_edit:
494 495 self.custom_edit_requested.emit(filename, line)
495 496 elif not self.editor:
496 497 self._append_plain_text('No default editor available.\n'
497 498 'Specify a GUI text editor in the `IPythonWidget.editor` '
498 499 'configurable to enable the %edit magic')
499 500 else:
500 501 try:
501 502 filename = '"%s"' % filename
502 503 if line and self.editor_line:
503 504 command = self.editor_line.format(filename=filename,
504 505 line=line)
505 506 else:
506 507 try:
507 508 command = self.editor.format()
508 509 except KeyError:
509 510 command = self.editor.format(filename=filename)
510 511 else:
511 512 command += ' ' + filename
512 513 except KeyError:
513 514 self._append_plain_text('Invalid editor command.\n')
514 515 else:
515 516 try:
516 517 Popen(command, shell=True)
517 518 except OSError:
518 519 msg = 'Opening editor with command "%s" failed.\n'
519 520 self._append_plain_text(msg % command)
520 521
521 522 def _make_in_prompt(self, number):
522 523 """ Given a prompt number, returns an HTML In prompt.
523 524 """
524 525 try:
525 526 body = self.in_prompt % number
526 527 except TypeError:
527 528 # allow in_prompt to leave out number, e.g. '>>> '
528 529 body = self.in_prompt
529 530 return '<span class="in-prompt">%s</span>' % body
530 531
531 532 def _make_continuation_prompt(self, prompt):
532 533 """ Given a plain text version of an In prompt, returns an HTML
533 534 continuation prompt.
534 535 """
535 536 end_chars = '...: '
536 537 space_count = len(prompt.lstrip('\n')) - len(end_chars)
537 538 body = '&nbsp;' * space_count + end_chars
538 539 return '<span class="in-prompt">%s</span>' % body
539 540
540 541 def _make_out_prompt(self, number):
541 542 """ Given a prompt number, returns an HTML Out prompt.
542 543 """
543 544 body = self.out_prompt % number
544 545 return '<span class="out-prompt">%s</span>' % body
545 546
546 547 #------ Payload handlers --------------------------------------------------
547 548
548 549 # Payload handlers with a generic interface: each takes the opaque payload
549 550 # dict, unpacks it and calls the underlying functions with the necessary
550 551 # arguments.
551 552
552 553 def _handle_payload_edit(self, item):
553 554 self._edit(item['filename'], item['line_number'])
554 555
555 556 def _handle_payload_exit(self, item):
556 557 self._keep_kernel_on_exit = item['keepkernel']
557 558 self.exit_requested.emit(self)
558 559
559 560 def _handle_payload_next_input(self, item):
560 561 self.input_buffer = item['text']
561 562
562 563 def _handle_payload_page(self, item):
563 564 # Since the plain text widget supports only a very small subset of HTML
564 565 # and we have no control over the HTML source, we only page HTML
565 566 # payloads in the rich text widget.
566 567 if item['html'] and self.kind == 'rich':
567 568 self._page(item['html'], html=True)
568 569 else:
569 570 self._page(item['text'], html=False)
570 571
571 572 #------ Trait change handlers --------------------------------------------
572 573
573 574 def _style_sheet_changed(self):
574 575 """ Set the style sheets of the underlying widgets.
575 576 """
576 577 self.setStyleSheet(self.style_sheet)
577 578 if self._control is not None:
578 579 self._control.document().setDefaultStyleSheet(self.style_sheet)
579 580 bg_color = self._control.palette().window().color()
580 581 self._ansi_processor.set_background_color(bg_color)
581 582
582 583 if self._page_control is not None:
583 584 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
584 585
585 586
586 587
587 588 def _syntax_style_changed(self):
588 589 """ Set the style for the syntax highlighter.
589 590 """
590 591 if self._highlighter is None:
591 592 # ignore premature calls
592 593 return
593 594 if self.syntax_style:
594 595 self._highlighter.set_style(self.syntax_style)
595 596 else:
596 597 self._highlighter.set_style_sheet(self.style_sheet)
597 598
598 599 #------ Trait default initializers -----------------------------------------
599 600
600 601 def _banner_default(self):
601 602 from IPython.core.usage import default_gui_banner
602 603 return default_gui_banner
@@ -1,341 +1,343 b''
1 1 #-----------------------------------------------------------------------------
2 2 # Copyright (c) 2010, IPython Development Team.
3 3 #
4 4 # Distributed under the terms of the Modified BSD License.
5 5 #
6 6 # The full license is in the file COPYING.txt, distributed with this software.
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard libary imports.
10 10 from base64 import decodestring
11 11 import os
12 12 import re
13 13
14 14 # System libary imports.
15 15 from IPython.external.qt import QtCore, QtGui
16 16
17 17 # Local imports
18 18 from IPython.utils.traitlets import Bool
19 19 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
20 20 from .ipython_widget import IPythonWidget
21 21
22 22
23 23 class RichIPythonWidget(IPythonWidget):
24 24 """ An IPythonWidget that supports rich text, including lists, images, and
25 25 tables. Note that raw performance will be reduced compared to the plain
26 26 text version.
27 27 """
28 28
29 29 # RichIPythonWidget protected class variables.
30 30 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
31 31 _jpg_supported = Bool(False)
32 32
33 33 # Used to determine whether a given html export attempt has already
34 34 # displayed a warning about being unable to convert a png to svg.
35 35 _svg_warning_displayed = False
36 36
37 37 #---------------------------------------------------------------------------
38 38 # 'object' interface
39 39 #---------------------------------------------------------------------------
40 40
41 41 def __init__(self, *args, **kw):
42 42 """ Create a RichIPythonWidget.
43 43 """
44 44 kw['kind'] = 'rich'
45 45 super(RichIPythonWidget, self).__init__(*args, **kw)
46 46
47 47 # Configure the ConsoleWidget HTML exporter for our formats.
48 48 self._html_exporter.image_tag = self._get_image_tag
49 49
50 50 # Dictionary for resolving document resource names to SVG data.
51 51 self._name_to_svg_map = {}
52 52
53 53 # Do we support jpg ?
54 54 # it seems that sometime jpg support is a plugin of QT, so try to assume
55 55 # it is not always supported.
56 56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
57 57 self._jpg_supported = 'jpeg' in _supported_format
58 58
59 59
60 60 #---------------------------------------------------------------------------
61 61 # 'ConsoleWidget' public interface overides
62 62 #---------------------------------------------------------------------------
63 63
64 64 def export_html(self):
65 65 """ Shows a dialog to export HTML/XML in various formats.
66 66
67 67 Overridden in order to reset the _svg_warning_displayed flag prior
68 68 to the export running.
69 69 """
70 70 self._svg_warning_displayed = False
71 71 super(RichIPythonWidget, self).export_html()
72 72
73 73
74 74 #---------------------------------------------------------------------------
75 75 # 'ConsoleWidget' protected interface
76 76 #---------------------------------------------------------------------------
77 77
78 78 def _context_menu_make(self, pos):
79 79 """ Reimplemented to return a custom context menu for images.
80 80 """
81 81 format = self._control.cursorForPosition(pos).charFormat()
82 82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
83 83 if name:
84 84 menu = QtGui.QMenu()
85 85
86 86 menu.addAction('Copy Image', lambda: self._copy_image(name))
87 87 menu.addAction('Save Image As...', lambda: self._save_image(name))
88 88 menu.addSeparator()
89 89
90 90 svg = self._name_to_svg_map.get(name, None)
91 91 if svg is not None:
92 92 menu.addSeparator()
93 93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
94 94 menu.addAction('Save SVG As...',
95 95 lambda: save_svg(svg, self._control))
96 96 else:
97 97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
98 98 return menu
99 99
100 100 #---------------------------------------------------------------------------
101 101 # 'BaseFrontendMixin' abstract interface
102 102 #---------------------------------------------------------------------------
103 103 def _pre_image_append(self, msg, prompt_number):
104 104 """ Append the Out[] prompt and make the output nicer
105 105
106 106 Shared code for some the following if statement
107 107 """
108 108 self.log.debug("pyout: %s", msg.get('content', ''))
109 109 self._append_plain_text(self.output_sep, True)
110 110 self._append_html(self._make_out_prompt(prompt_number), True)
111 111 self._append_plain_text('\n', True)
112 112
113 113 def _handle_pyout(self, msg):
114 114 """ Overridden to handle rich data types, like SVG.
115 115 """
116 116 if not self._hidden and self._is_from_this_session(msg):
117 self.flush_clearoutput()
117 118 content = msg['content']
118 119 prompt_number = content.get('execution_count', 0)
119 120 data = content['data']
120 121 metadata = msg['content']['metadata']
121 122 if 'image/svg+xml' in data:
122 123 self._pre_image_append(msg, prompt_number)
123 124 self._append_svg(data['image/svg+xml'], True)
124 125 self._append_html(self.output_sep2, True)
125 126 elif 'image/png' in data:
126 127 self._pre_image_append(msg, prompt_number)
127 128 png = decodestring(data['image/png'].encode('ascii'))
128 129 self._append_png(png, True, metadata=metadata.get('image/png', None))
129 130 self._append_html(self.output_sep2, True)
130 131 elif 'image/jpeg' in data and self._jpg_supported:
131 132 self._pre_image_append(msg, prompt_number)
132 133 jpg = decodestring(data['image/jpeg'].encode('ascii'))
133 134 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
134 135 self._append_html(self.output_sep2, True)
135 136 else:
136 137 # Default back to the plain text representation.
137 138 return super(RichIPythonWidget, self)._handle_pyout(msg)
138 139
139 140 def _handle_display_data(self, msg):
140 141 """ Overridden to handle rich data types, like SVG.
141 142 """
142 143 if not self._hidden and self._is_from_this_session(msg):
144 self.flush_clearoutput()
143 145 source = msg['content']['source']
144 146 data = msg['content']['data']
145 147 metadata = msg['content']['metadata']
146 148 # Try to use the svg or html representations.
147 149 # FIXME: Is this the right ordering of things to try?
148 150 if 'image/svg+xml' in data:
149 151 self.log.debug("display: %s", msg.get('content', ''))
150 152 svg = data['image/svg+xml']
151 153 self._append_svg(svg, True)
152 154 elif 'image/png' in data:
153 155 self.log.debug("display: %s", msg.get('content', ''))
154 156 # PNG data is base64 encoded as it passes over the network
155 157 # in a JSON structure so we decode it.
156 158 png = decodestring(data['image/png'].encode('ascii'))
157 159 self._append_png(png, True, metadata=metadata.get('image/png', None))
158 160 elif 'image/jpeg' in data and self._jpg_supported:
159 161 self.log.debug("display: %s", msg.get('content', ''))
160 162 jpg = decodestring(data['image/jpeg'].encode('ascii'))
161 163 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
162 164 else:
163 165 # Default back to the plain text representation.
164 166 return super(RichIPythonWidget, self)._handle_display_data(msg)
165 167
166 168 #---------------------------------------------------------------------------
167 169 # 'RichIPythonWidget' protected interface
168 170 #---------------------------------------------------------------------------
169 171
170 172 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
171 173 """ Append raw JPG data to the widget."""
172 174 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
173 175
174 176 def _append_png(self, png, before_prompt=False, metadata=None):
175 177 """ Append raw PNG data to the widget.
176 178 """
177 179 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
178 180
179 181 def _append_svg(self, svg, before_prompt=False):
180 182 """ Append raw SVG data to the widget.
181 183 """
182 184 self._append_custom(self._insert_svg, svg, before_prompt)
183 185
184 186 def _add_image(self, image):
185 187 """ Adds the specified QImage to the document and returns a
186 188 QTextImageFormat that references it.
187 189 """
188 190 document = self._control.document()
189 191 name = str(image.cacheKey())
190 192 document.addResource(QtGui.QTextDocument.ImageResource,
191 193 QtCore.QUrl(name), image)
192 194 format = QtGui.QTextImageFormat()
193 195 format.setName(name)
194 196 return format
195 197
196 198 def _copy_image(self, name):
197 199 """ Copies the ImageResource with 'name' to the clipboard.
198 200 """
199 201 image = self._get_image(name)
200 202 QtGui.QApplication.clipboard().setImage(image)
201 203
202 204 def _get_image(self, name):
203 205 """ Returns the QImage stored as the ImageResource with 'name'.
204 206 """
205 207 document = self._control.document()
206 208 image = document.resource(QtGui.QTextDocument.ImageResource,
207 209 QtCore.QUrl(name))
208 210 return image
209 211
210 212 def _get_image_tag(self, match, path = None, format = "png"):
211 213 """ Return (X)HTML mark-up for the image-tag given by match.
212 214
213 215 Parameters
214 216 ----------
215 217 match : re.SRE_Match
216 218 A match to an HTML image tag as exported by Qt, with
217 219 match.group("Name") containing the matched image ID.
218 220
219 221 path : string|None, optional [default None]
220 222 If not None, specifies a path to which supporting files may be
221 223 written (e.g., for linked images). If None, all images are to be
222 224 included inline.
223 225
224 226 format : "png"|"svg"|"jpg", optional [default "png"]
225 227 Format for returned or referenced images.
226 228 """
227 229 if format in ("png","jpg"):
228 230 try:
229 231 image = self._get_image(match.group("name"))
230 232 except KeyError:
231 233 return "<b>Couldn't find image %s</b>" % match.group("name")
232 234
233 235 if path is not None:
234 236 if not os.path.exists(path):
235 237 os.mkdir(path)
236 238 relpath = os.path.basename(path)
237 239 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
238 240 "PNG"):
239 241 return '<img src="%s/qt_img%s.%s">' % (relpath,
240 242 match.group("name"),format)
241 243 else:
242 244 return "<b>Couldn't save image!</b>"
243 245 else:
244 246 ba = QtCore.QByteArray()
245 247 buffer_ = QtCore.QBuffer(ba)
246 248 buffer_.open(QtCore.QIODevice.WriteOnly)
247 249 image.save(buffer_, format.upper())
248 250 buffer_.close()
249 251 return '<img src="data:image/%s;base64,\n%s\n" />' % (
250 252 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
251 253
252 254 elif format == "svg":
253 255 try:
254 256 svg = str(self._name_to_svg_map[match.group("name")])
255 257 except KeyError:
256 258 if not self._svg_warning_displayed:
257 259 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
258 260 'Cannot convert PNG images to SVG, export with PNG figures instead. '
259 261 'If you want to export matplotlib figures as SVG, add '
260 262 'to your ipython config:\n\n'
261 263 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
262 264 'And regenerate the figures.',
263 265 QtGui.QMessageBox.Ok)
264 266 self._svg_warning_displayed = True
265 267 return ("<b>Cannot convert PNG images to SVG.</b> "
266 268 "You must export this session with PNG images. "
267 269 "If you want to export matplotlib figures as SVG, add to your config "
268 270 "<span>c.InlineBackend.figure_format = 'svg'</span> "
269 271 "and regenerate the figures.")
270 272
271 273 # Not currently checking path, because it's tricky to find a
272 274 # cross-browser way to embed external SVG images (e.g., via
273 275 # object or embed tags).
274 276
275 277 # Chop stand-alone header from matplotlib SVG
276 278 offset = svg.find("<svg")
277 279 assert(offset > -1)
278 280
279 281 return svg[offset:]
280 282
281 283 else:
282 284 return '<b>Unrecognized image format</b>'
283 285
284 286 def _insert_jpg(self, cursor, jpg, metadata=None):
285 287 """ Insert raw PNG data into the widget."""
286 288 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
287 289
288 290 def _insert_png(self, cursor, png, metadata=None):
289 291 """ Insert raw PNG data into the widget.
290 292 """
291 293 self._insert_img(cursor, png, 'png', metadata=metadata)
292 294
293 295 def _insert_img(self, cursor, img, fmt, metadata=None):
294 296 """ insert a raw image, jpg or png """
295 297 if metadata:
296 298 width = metadata.get('width', None)
297 299 height = metadata.get('height', None)
298 300 else:
299 301 width = height = None
300 302 try:
301 303 image = QtGui.QImage()
302 304 image.loadFromData(img, fmt.upper())
303 305 if width and height:
304 306 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
305 307 elif width and not height:
306 308 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
307 309 elif height and not width:
308 310 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
309 311 except ValueError:
310 312 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
311 313 else:
312 314 format = self._add_image(image)
313 315 cursor.insertBlock()
314 316 cursor.insertImage(format)
315 317 cursor.insertBlock()
316 318
317 319 def _insert_svg(self, cursor, svg):
318 320 """ Insert raw SVG data into the widet.
319 321 """
320 322 try:
321 323 image = svg_to_image(svg)
322 324 except ValueError:
323 325 self._insert_plain_text(cursor, 'Received invalid SVG data.')
324 326 else:
325 327 format = self._add_image(image)
326 328 self._name_to_svg_map[format.name()] = svg
327 329 cursor.insertBlock()
328 330 cursor.insertImage(format)
329 331 cursor.insertBlock()
330 332
331 333 def _save_image(self, name, format='PNG'):
332 334 """ Shows a save dialog for the ImageResource with 'name'.
333 335 """
334 336 dialog = QtGui.QFileDialog(self._control, 'Save Image')
335 337 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
336 338 dialog.setDefaultSuffix(format.lower())
337 339 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
338 340 if dialog.exec_():
339 341 filename = dialog.selectedFiles()[0]
340 342 image = self._get_image(name)
341 343 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now