frontend_widget.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793
  1. """Frontend widget for the Qt Console"""
  2. # Copyright (c) Jupyter Development Team
  3. # Distributed under the terms of the Modified BSD License.
  4. from __future__ import print_function
  5. from collections import namedtuple
  6. import sys
  7. import uuid
  8. import re
  9. from qtpy import QtCore, QtGui, QtWidgets
  10. from ipython_genutils import py3compat
  11. from ipython_genutils.importstring import import_item
  12. from qtconsole.base_frontend_mixin import BaseFrontendMixin
  13. from traitlets import Any, Bool, Instance, Unicode, DottedObjectName, default
  14. from .bracket_matcher import BracketMatcher
  15. from .call_tip_widget import CallTipWidget
  16. from .history_console_widget import HistoryConsoleWidget
  17. from .pygments_highlighter import PygmentsHighlighter
  18. class FrontendHighlighter(PygmentsHighlighter):
  19. """ A PygmentsHighlighter that understands and ignores prompts.
  20. """
  21. def __init__(self, frontend, lexer=None):
  22. super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
  23. self._current_offset = 0
  24. self._frontend = frontend
  25. self.highlighting_on = False
  26. self._classic_prompt_re = re.compile(
  27. r'^(%s)?([ \t]*>>> |^[ \t]*\.\.\. )' % re.escape(frontend.other_output_prefix)
  28. )
  29. self._ipy_prompt_re = re.compile(
  30. r'^(%s)?([ \t]*In \[\d+\]: |[ \t]*\ \ \ \.\.\.+: )' % re.escape(frontend.other_output_prefix)
  31. )
  32. def transform_classic_prompt(self, line):
  33. """Handle inputs that start with '>>> ' syntax."""
  34. if not line or line.isspace():
  35. return line
  36. m = self._classic_prompt_re.match(line)
  37. if m:
  38. return line[len(m.group(0)):]
  39. else:
  40. return line
  41. def transform_ipy_prompt(self, line):
  42. """Handle inputs that start classic IPython prompt syntax."""
  43. if not line or line.isspace():
  44. return line
  45. m = self._ipy_prompt_re.match(line)
  46. if m:
  47. return line[len(m.group(0)):]
  48. else:
  49. return line
  50. def highlightBlock(self, string):
  51. """ Highlight a block of text. Reimplemented to highlight selectively.
  52. """
  53. if not hasattr(self, 'highlighting_on') or not self.highlighting_on:
  54. return
  55. # The input to this function is a unicode string that may contain
  56. # paragraph break characters, non-breaking spaces, etc. Here we acquire
  57. # the string as plain text so we can compare it.
  58. current_block = self.currentBlock()
  59. string = self._frontend._get_block_plain_text(current_block)
  60. # Only highlight if we can identify a prompt, but make sure not to
  61. # highlight the prompt.
  62. without_prompt = self.transform_ipy_prompt(string)
  63. diff = len(string) - len(without_prompt)
  64. if diff > 0:
  65. self._current_offset = diff
  66. super(FrontendHighlighter, self).highlightBlock(without_prompt)
  67. def rehighlightBlock(self, block):
  68. """ Reimplemented to temporarily enable highlighting if disabled.
  69. """
  70. old = self.highlighting_on
  71. self.highlighting_on = True
  72. super(FrontendHighlighter, self).rehighlightBlock(block)
  73. self.highlighting_on = old
  74. def setFormat(self, start, count, format):
  75. """ Reimplemented to highlight selectively.
  76. """
  77. start += self._current_offset
  78. super(FrontendHighlighter, self).setFormat(start, count, format)
  79. class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
  80. """ A Qt frontend for a generic Python kernel.
  81. """
  82. # The text to show when the kernel is (re)started.
  83. banner = Unicode(config=True)
  84. kernel_banner = Unicode()
  85. # Whether to show the banner
  86. _display_banner = Bool(False)
  87. # An option and corresponding signal for overriding the default kernel
  88. # interrupt behavior.
  89. custom_interrupt = Bool(False)
  90. custom_interrupt_requested = QtCore.Signal()
  91. # An option and corresponding signals for overriding the default kernel
  92. # restart behavior.
  93. custom_restart = Bool(False)
  94. custom_restart_kernel_died = QtCore.Signal(float)
  95. custom_restart_requested = QtCore.Signal()
  96. # Whether to automatically show calltips on open-parentheses.
  97. enable_calltips = Bool(True, config=True,
  98. help="Whether to draw information calltips on open-parentheses.")
  99. clear_on_kernel_restart = Bool(True, config=True,
  100. help="Whether to clear the console when the kernel is restarted")
  101. confirm_restart = Bool(True, config=True,
  102. help="Whether to ask for user confirmation when restarting kernel")
  103. lexer_class = DottedObjectName(config=True,
  104. help="The pygments lexer class to use."
  105. )
  106. def _lexer_class_changed(self, name, old, new):
  107. lexer_class = import_item(new)
  108. self.lexer = lexer_class()
  109. def _lexer_class_default(self):
  110. if py3compat.PY3:
  111. return 'pygments.lexers.Python3Lexer'
  112. else:
  113. return 'pygments.lexers.PythonLexer'
  114. lexer = Any()
  115. def _lexer_default(self):
  116. lexer_class = import_item(self.lexer_class)
  117. return lexer_class()
  118. # Emitted when a user visible 'execute_request' has been submitted to the
  119. # kernel from the FrontendWidget. Contains the code to be executed.
  120. executing = QtCore.Signal(object)
  121. # Emitted when a user-visible 'execute_reply' has been received from the
  122. # kernel and processed by the FrontendWidget. Contains the response message.
  123. executed = QtCore.Signal(object)
  124. # Emitted when an exit request has been received from the kernel.
  125. exit_requested = QtCore.Signal(object)
  126. _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
  127. _CompletionRequest = namedtuple('_CompletionRequest',
  128. ['id', 'code', 'pos'])
  129. _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
  130. _local_kernel = False
  131. _highlighter = Instance(FrontendHighlighter, allow_none=True)
  132. # -------------------------------------------------------------------------
  133. # 'Object' interface
  134. # -------------------------------------------------------------------------
  135. def __init__(self, local_kernel=_local_kernel, *args, **kw):
  136. super(FrontendWidget, self).__init__(*args, **kw)
  137. # FrontendWidget protected variables.
  138. self._bracket_matcher = BracketMatcher(self._control)
  139. self._call_tip_widget = CallTipWidget(self._control)
  140. self._copy_raw_action = QtWidgets.QAction('Copy (Raw Text)', None)
  141. self._hidden = False
  142. self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
  143. self._kernel_manager = None
  144. self._kernel_client = None
  145. self._request_info = {}
  146. self._request_info['execute'] = {}
  147. self._callback_dict = {}
  148. self._display_banner = True
  149. # Configure the ConsoleWidget.
  150. self.tab_width = 4
  151. self._set_continuation_prompt('... ')
  152. # Configure the CallTipWidget.
  153. self._call_tip_widget.setFont(self.font)
  154. self.font_changed.connect(self._call_tip_widget.setFont)
  155. # Configure actions.
  156. action = self._copy_raw_action
  157. key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
  158. action.setEnabled(False)
  159. action.setShortcut(QtGui.QKeySequence(key))
  160. action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
  161. action.triggered.connect(self.copy_raw)
  162. self.copy_available.connect(action.setEnabled)
  163. self.addAction(action)
  164. # Connect signal handlers.
  165. document = self._control.document()
  166. document.contentsChange.connect(self._document_contents_change)
  167. # Set flag for whether we are connected via localhost.
  168. self._local_kernel = local_kernel
  169. # Whether or not a clear_output call is pending new output.
  170. self._pending_clearoutput = False
  171. #---------------------------------------------------------------------------
  172. # 'ConsoleWidget' public interface
  173. #---------------------------------------------------------------------------
  174. def copy(self):
  175. """ Copy the currently selected text to the clipboard, removing prompts.
  176. """
  177. if self._page_control is not None and self._page_control.hasFocus():
  178. self._page_control.copy()
  179. elif self._control.hasFocus():
  180. text = self._control.textCursor().selection().toPlainText()
  181. if text:
  182. # Remove prompts.
  183. lines = text.splitlines()
  184. lines = map(self._highlighter.transform_classic_prompt, lines)
  185. lines = map(self._highlighter.transform_ipy_prompt, lines)
  186. text = '\n'.join(lines)
  187. # Needed to prevent errors when copying the prompt.
  188. # See issue 264
  189. try:
  190. was_newline = text[-1] == '\n'
  191. except IndexError:
  192. was_newline = False
  193. if was_newline: # user doesn't need newline
  194. text = text[:-1]
  195. QtWidgets.QApplication.clipboard().setText(text)
  196. else:
  197. self.log.debug("frontend widget : unknown copy target")
  198. #---------------------------------------------------------------------------
  199. # 'ConsoleWidget' abstract interface
  200. #---------------------------------------------------------------------------
  201. def _execute(self, source, hidden):
  202. """ Execute 'source'. If 'hidden', do not show any output.
  203. See parent class :meth:`execute` docstring for full details.
  204. """
  205. msg_id = self.kernel_client.execute(source, hidden)
  206. self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
  207. self._hidden = hidden
  208. if not hidden:
  209. self.executing.emit(source)
  210. def _prompt_started_hook(self):
  211. """ Called immediately after a new prompt is displayed.
  212. """
  213. if not self._reading:
  214. self._highlighter.highlighting_on = True
  215. def _prompt_finished_hook(self):
  216. """ Called immediately after a prompt is finished, i.e. when some input
  217. will be processed and a new prompt displayed.
  218. """
  219. if not self._reading:
  220. self._highlighter.highlighting_on = False
  221. def _tab_pressed(self):
  222. """ Called when the tab key is pressed. Returns whether to continue
  223. processing the event.
  224. """
  225. # Perform tab completion if:
  226. # 1) The cursor is in the input buffer.
  227. # 2) There is a non-whitespace character before the cursor.
  228. # 3) There is no active selection.
  229. text = self._get_input_buffer_cursor_line()
  230. if text is None:
  231. return False
  232. non_ws_before = bool(text[:self._get_input_buffer_cursor_column()].strip())
  233. complete = non_ws_before and self._get_cursor().selectedText() == ''
  234. if complete:
  235. self._complete()
  236. return not complete
  237. #---------------------------------------------------------------------------
  238. # 'ConsoleWidget' protected interface
  239. #---------------------------------------------------------------------------
  240. def _context_menu_make(self, pos):
  241. """ Reimplemented to add an action for raw copy.
  242. """
  243. menu = super(FrontendWidget, self)._context_menu_make(pos)
  244. for before_action in menu.actions():
  245. if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
  246. QtGui.QKeySequence.ExactMatch:
  247. menu.insertAction(before_action, self._copy_raw_action)
  248. break
  249. return menu
  250. def request_interrupt_kernel(self):
  251. if self._executing:
  252. self.interrupt_kernel()
  253. def request_restart_kernel(self):
  254. message = 'Are you sure you want to restart the kernel?'
  255. self.restart_kernel(message, now=False)
  256. def _event_filter_console_keypress(self, event):
  257. """ Reimplemented for execution interruption and smart backspace.
  258. """
  259. key = event.key()
  260. if self._control_key_down(event.modifiers(), include_command=False):
  261. if key == QtCore.Qt.Key_C and self._executing:
  262. self.request_interrupt_kernel()
  263. return True
  264. elif key == QtCore.Qt.Key_Period:
  265. self.request_restart_kernel()
  266. return True
  267. elif not event.modifiers() & QtCore.Qt.AltModifier:
  268. # Smart backspace: remove four characters in one backspace if:
  269. # 1) everything left of the cursor is whitespace
  270. # 2) the four characters immediately left of the cursor are spaces
  271. if key == QtCore.Qt.Key_Backspace:
  272. col = self._get_input_buffer_cursor_column()
  273. cursor = self._control.textCursor()
  274. if col > 3 and not cursor.hasSelection():
  275. text = self._get_input_buffer_cursor_line()[:col]
  276. if text.endswith(' ') and not text.strip():
  277. cursor.movePosition(QtGui.QTextCursor.Left,
  278. QtGui.QTextCursor.KeepAnchor, 4)
  279. cursor.removeSelectedText()
  280. return True
  281. return super(FrontendWidget, self)._event_filter_console_keypress(event)
  282. #---------------------------------------------------------------------------
  283. # 'BaseFrontendMixin' abstract interface
  284. #---------------------------------------------------------------------------
  285. def _handle_clear_output(self, msg):
  286. """Handle clear output messages."""
  287. if self.include_output(msg):
  288. wait = msg['content'].get('wait', True)
  289. if wait:
  290. self._pending_clearoutput = True
  291. else:
  292. self.clear_output()
  293. def _silent_exec_callback(self, expr, callback):
  294. """Silently execute `expr` in the kernel and call `callback` with reply
  295. the `expr` is evaluated silently in the kernel (without) output in
  296. the frontend. Call `callback` with the
  297. `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
  298. Parameters
  299. ----------
  300. expr : string
  301. valid string to be executed by the kernel.
  302. callback : function
  303. function accepting one argument, as a string. The string will be
  304. the `repr` of the result of evaluating `expr`
  305. The `callback` is called with the `repr()` of the result of `expr` as
  306. first argument. To get the object, do `eval()` on the passed value.
  307. See Also
  308. --------
  309. _handle_exec_callback : private method, deal with calling callback with reply
  310. """
  311. # generate uuid, which would be used as an indication of whether or
  312. # not the unique request originated from here (can use msg id ?)
  313. local_uuid = str(uuid.uuid1())
  314. msg_id = self.kernel_client.execute('',
  315. silent=True, user_expressions={ local_uuid:expr })
  316. self._callback_dict[local_uuid] = callback
  317. self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
  318. def _handle_exec_callback(self, msg):
  319. """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
  320. Parameters
  321. ----------
  322. msg : raw message send by the kernel containing an `user_expressions`
  323. and having a 'silent_exec_callback' kind.
  324. Notes
  325. -----
  326. This function will look for a `callback` associated with the
  327. corresponding message id. Association has been made by
  328. `_silent_exec_callback`. `callback` is then called with the `repr()`
  329. of the value of corresponding `user_expressions` as argument.
  330. `callback` is then removed from the known list so that any message
  331. coming again with the same id won't trigger it.
  332. """
  333. user_exp = msg['content'].get('user_expressions')
  334. if not user_exp:
  335. return
  336. for expression in user_exp:
  337. if expression in self._callback_dict:
  338. self._callback_dict.pop(expression)(user_exp[expression])
  339. def _handle_execute_reply(self, msg):
  340. """ Handles replies for code execution.
  341. """
  342. self.log.debug("execute_reply: %s", msg.get('content', ''))
  343. msg_id = msg['parent_header']['msg_id']
  344. info = self._request_info['execute'].get(msg_id)
  345. # unset reading flag, because if execute finished, raw_input can't
  346. # still be pending.
  347. self._reading = False
  348. # Note: If info is NoneType, this is ignored
  349. if info and info.kind == 'user' and not self._hidden:
  350. # Make sure that all output from the SUB channel has been processed
  351. # before writing a new prompt.
  352. self.kernel_client.iopub_channel.flush()
  353. # Reset the ANSI style information to prevent bad text in stdout
  354. # from messing up our colors. We're not a true terminal so we're
  355. # allowed to do this.
  356. if self.ansi_codes:
  357. self._ansi_processor.reset_sgr()
  358. content = msg['content']
  359. status = content['status']
  360. if status == 'ok':
  361. self._process_execute_ok(msg)
  362. elif status == 'aborted':
  363. self._process_execute_abort(msg)
  364. self._show_interpreter_prompt_for_reply(msg)
  365. self.executed.emit(msg)
  366. self._request_info['execute'].pop(msg_id)
  367. elif info and info.kind == 'silent_exec_callback' and not self._hidden:
  368. self._handle_exec_callback(msg)
  369. self._request_info['execute'].pop(msg_id)
  370. elif info and not self._hidden:
  371. raise RuntimeError("Unknown handler for %s" % info.kind)
  372. def _handle_error(self, msg):
  373. """ Handle error messages.
  374. """
  375. self._process_execute_error(msg)
  376. def _handle_input_request(self, msg):
  377. """ Handle requests for raw_input.
  378. """
  379. self.log.debug("input: %s", msg.get('content', ''))
  380. if self._hidden:
  381. raise RuntimeError('Request for raw input during hidden execution.')
  382. # Make sure that all output from the SUB channel has been processed
  383. # before entering readline mode.
  384. self.kernel_client.iopub_channel.flush()
  385. def callback(line):
  386. self._finalize_input_request()
  387. self.kernel_client.input(line)
  388. if self._reading:
  389. self.log.debug("Got second input request, assuming first was interrupted.")
  390. self._reading = False
  391. self._readline(msg['content']['prompt'], callback=callback, password=msg['content']['password'])
  392. def _kernel_restarted_message(self, died=True):
  393. msg = "Kernel died, restarting" if died else "Kernel restarting"
  394. self._append_html("<br>%s<hr><br>" % msg,
  395. before_prompt=False
  396. )
  397. def _handle_kernel_died(self, since_last_heartbeat):
  398. """Handle the kernel's death (if we do not own the kernel).
  399. """
  400. self.log.warning("kernel died: %s", since_last_heartbeat)
  401. if self.custom_restart:
  402. self.custom_restart_kernel_died.emit(since_last_heartbeat)
  403. else:
  404. self._kernel_restarted_message(died=True)
  405. self.reset()
  406. def _handle_kernel_restarted(self, died=True):
  407. """Notice that the autorestarter restarted the kernel.
  408. There's nothing to do but show a message.
  409. """
  410. self.log.warning("kernel restarted")
  411. self._kernel_restarted_message(died=died)
  412. self.reset()
  413. def _handle_inspect_reply(self, rep):
  414. """Handle replies for call tips."""
  415. self.log.debug("oinfo: %s", rep.get('content', ''))
  416. cursor = self._get_cursor()
  417. info = self._request_info.get('call_tip')
  418. if info and info.id == rep['parent_header']['msg_id'] and \
  419. info.pos == cursor.position():
  420. content = rep['content']
  421. if content.get('status') == 'ok' and content.get('found', False):
  422. self._call_tip_widget.show_inspect_data(content)
  423. def _handle_execute_result(self, msg):
  424. """ Handle display hook output.
  425. """
  426. self.log.debug("execute_result: %s", msg.get('content', ''))
  427. if self.include_output(msg):
  428. self.flush_clearoutput()
  429. text = msg['content']['data']
  430. self._append_plain_text(text + '\n', before_prompt=True)
  431. def _handle_stream(self, msg):
  432. """ Handle stdout, stderr, and stdin.
  433. """
  434. self.log.debug("stream: %s", msg.get('content', ''))
  435. if self.include_output(msg):
  436. self.flush_clearoutput()
  437. self.append_stream(msg['content']['text'])
  438. def _handle_shutdown_reply(self, msg):
  439. """ Handle shutdown signal, only if from other console.
  440. """
  441. self.log.debug("shutdown: %s", msg.get('content', ''))
  442. restart = msg.get('content', {}).get('restart', False)
  443. if not self._hidden and not self.from_here(msg):
  444. # got shutdown reply, request came from session other than ours
  445. if restart:
  446. # someone restarted the kernel, handle it
  447. self._handle_kernel_restarted(died=False)
  448. else:
  449. # kernel was shutdown permanently
  450. # this triggers exit_requested if the kernel was local,
  451. # and a dialog if the kernel was remote,
  452. # so we don't suddenly clear the qtconsole without asking.
  453. if self._local_kernel:
  454. self.exit_requested.emit(self)
  455. else:
  456. title = self.window().windowTitle()
  457. reply = QtWidgets.QMessageBox.question(self, title,
  458. "Kernel has been shutdown permanently. "
  459. "Close the Console?",
  460. QtWidgets.QMessageBox.Yes,QtWidgets.QMessageBox.No)
  461. if reply == QtWidgets.QMessageBox.Yes:
  462. self.exit_requested.emit(self)
  463. def _handle_status(self, msg):
  464. """Handle status message"""
  465. # This is where a busy/idle indicator would be triggered,
  466. # when we make one.
  467. state = msg['content'].get('execution_state', '')
  468. if state == 'starting':
  469. # kernel started while we were running
  470. if self._executing:
  471. self._handle_kernel_restarted(died=True)
  472. elif state == 'idle':
  473. pass
  474. elif state == 'busy':
  475. pass
  476. def _started_channels(self):
  477. """ Called when the KernelManager channels have started listening or
  478. when the frontend is assigned an already listening KernelManager.
  479. """
  480. self.reset(clear=True)
  481. #---------------------------------------------------------------------------
  482. # 'FrontendWidget' public interface
  483. #---------------------------------------------------------------------------
  484. def copy_raw(self):
  485. """ Copy the currently selected text to the clipboard without attempting
  486. to remove prompts or otherwise alter the text.
  487. """
  488. self._control.copy()
  489. def interrupt_kernel(self):
  490. """ Attempts to interrupt the running kernel.
  491. Also unsets _reading flag, to avoid runtime errors
  492. if raw_input is called again.
  493. """
  494. if self.custom_interrupt:
  495. self._reading = False
  496. self.custom_interrupt_requested.emit()
  497. elif self.kernel_manager:
  498. self._reading = False
  499. self.kernel_manager.interrupt_kernel()
  500. else:
  501. self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
  502. def reset(self, clear=False):
  503. """ Resets the widget to its initial state if ``clear`` parameter
  504. is True, otherwise
  505. prints a visual indication of the fact that the kernel restarted, but
  506. does not clear the traces from previous usage of the kernel before it
  507. was restarted. With ``clear=True``, it is similar to ``%clear``, but
  508. also re-writes the banner and aborts execution if necessary.
  509. """
  510. if self._executing:
  511. self._executing = False
  512. self._request_info['execute'] = {}
  513. self._reading = False
  514. self._highlighter.highlighting_on = False
  515. if clear:
  516. self._control.clear()
  517. if self._display_banner:
  518. self._append_plain_text(self.banner)
  519. if self.kernel_banner:
  520. self._append_plain_text(self.kernel_banner)
  521. # update output marker for stdout/stderr, so that startup
  522. # messages appear after banner:
  523. self._show_interpreter_prompt()
  524. def restart_kernel(self, message, now=False):
  525. """ Attempts to restart the running kernel.
  526. """
  527. # FIXME: now should be configurable via a checkbox in the dialog. Right
  528. # now at least the heartbeat path sets it to True and the manual restart
  529. # to False. But those should just be the pre-selected states of a
  530. # checkbox that the user could override if so desired. But I don't know
  531. # enough Qt to go implementing the checkbox now.
  532. if self.custom_restart:
  533. self.custom_restart_requested.emit()
  534. return
  535. if self.kernel_manager:
  536. # Pause the heart beat channel to prevent further warnings.
  537. self.kernel_client.hb_channel.pause()
  538. # Prompt the user to restart the kernel. Un-pause the heartbeat if
  539. # they decline. (If they accept, the heartbeat will be un-paused
  540. # automatically when the kernel is restarted.)
  541. if self.confirm_restart:
  542. buttons = QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
  543. result = QtWidgets.QMessageBox.question(self, 'Restart kernel?',
  544. message, buttons)
  545. do_restart = result == QtWidgets.QMessageBox.Yes
  546. else:
  547. # confirm_restart is False, so we don't need to ask user
  548. # anything, just do the restart
  549. do_restart = True
  550. if do_restart:
  551. try:
  552. self.kernel_manager.restart_kernel(now=now)
  553. except RuntimeError as e:
  554. self._append_plain_text(
  555. 'Error restarting kernel: %s\n' % e,
  556. before_prompt=True
  557. )
  558. else:
  559. self._append_html("<br>Restarting kernel...\n<hr><br>",
  560. before_prompt=True,
  561. )
  562. else:
  563. self.kernel_client.hb_channel.unpause()
  564. else:
  565. self._append_plain_text(
  566. 'Cannot restart a Kernel I did not start\n',
  567. before_prompt=True
  568. )
  569. def append_stream(self, text):
  570. """Appends text to the output stream."""
  571. self._append_plain_text(text, before_prompt=True)
  572. def flush_clearoutput(self):
  573. """If a clearoutput is pending, execute it."""
  574. if self._pending_clearoutput:
  575. self._pending_clearoutput = False
  576. self.clear_output()
  577. def clear_output(self):
  578. """Clears the current line of output."""
  579. cursor = self._control.textCursor()
  580. cursor.beginEditBlock()
  581. cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
  582. cursor.insertText('')
  583. cursor.endEditBlock()
  584. #---------------------------------------------------------------------------
  585. # 'FrontendWidget' protected interface
  586. #---------------------------------------------------------------------------
  587. def _auto_call_tip(self):
  588. """Trigger call tip automatically on open parenthesis
  589. Call tips can be requested explcitly with `_call_tip`.
  590. """
  591. cursor = self._get_cursor()
  592. cursor.movePosition(QtGui.QTextCursor.Left)
  593. if cursor.document().characterAt(cursor.position()) == '(':
  594. # trigger auto call tip on open paren
  595. self._call_tip()
  596. def _call_tip(self):
  597. """Shows a call tip, if appropriate, at the current cursor location."""
  598. # Decide if it makes sense to show a call tip
  599. if not self.enable_calltips or not self.kernel_client.shell_channel.is_alive():
  600. return False
  601. cursor_pos = self._get_input_buffer_cursor_pos()
  602. code = self.input_buffer
  603. # Send the metadata request to the kernel
  604. msg_id = self.kernel_client.inspect(code, cursor_pos)
  605. pos = self._get_cursor().position()
  606. self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
  607. return True
  608. def _complete(self):
  609. """ Performs completion at the current cursor location.
  610. """
  611. code = self.input_buffer
  612. cursor_pos = self._get_input_buffer_cursor_pos()
  613. # Send the completion request to the kernel
  614. msg_id = self.kernel_client.complete(code=code, cursor_pos=cursor_pos)
  615. info = self._CompletionRequest(msg_id, code, cursor_pos)
  616. self._request_info['complete'] = info
  617. def _process_execute_abort(self, msg):
  618. """ Process a reply for an aborted execution request.
  619. """
  620. self._append_plain_text("ERROR: execution aborted\n")
  621. def _process_execute_error(self, msg):
  622. """ Process a reply for an execution request that resulted in an error.
  623. """
  624. content = msg['content']
  625. # If a SystemExit is passed along, this means exit() was called - also
  626. # all the ipython %exit magic syntax of '-k' to be used to keep
  627. # the kernel running
  628. if content['ename']=='SystemExit':
  629. keepkernel = content['evalue']=='-k' or content['evalue']=='True'
  630. self._keep_kernel_on_exit = keepkernel
  631. self.exit_requested.emit(self)
  632. else:
  633. traceback = ''.join(content['traceback'])
  634. self._append_plain_text(traceback)
  635. def _process_execute_ok(self, msg):
  636. """ Process a reply for a successful execution request.
  637. """
  638. payload = msg['content'].get('payload', [])
  639. for item in payload:
  640. if not self._process_execute_payload(item):
  641. warning = 'Warning: received unknown payload of type %s'
  642. print(warning % repr(item['source']))
  643. def _process_execute_payload(self, item):
  644. """ Process a single payload item from the list of payload items in an
  645. execution reply. Returns whether the payload was handled.
  646. """
  647. # The basic FrontendWidget doesn't handle payloads, as they are a
  648. # mechanism for going beyond the standard Python interpreter model.
  649. return False
  650. def _show_interpreter_prompt(self):
  651. """ Shows a prompt for the interpreter.
  652. """
  653. self._show_prompt('>>> ')
  654. def _show_interpreter_prompt_for_reply(self, msg):
  655. """ Shows a prompt for the interpreter given an 'execute_reply' message.
  656. """
  657. self._show_interpreter_prompt()
  658. #------ Signal handlers ----------------------------------------------------
  659. def _document_contents_change(self, position, removed, added):
  660. """ Called whenever the document's content changes. Display a call tip
  661. if appropriate.
  662. """
  663. # Calculate where the cursor should be *after* the change:
  664. position += added
  665. document = self._control.document()
  666. if position == self._get_cursor().position():
  667. self._auto_call_tip()
  668. #------ Trait default initializers -----------------------------------------
  669. @default('banner')
  670. def _banner_default(self):
  671. """ Returns the standard Python banner.
  672. """
  673. banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
  674. '"license" for more information.'
  675. return banner % (sys.version, sys.platform)