1# -*- coding:utf-8 -*- 2# 3# Copyright © Spyder Project Contributors 4# Licensed under the terms of the MIT License 5# (see spyder/__init__.py for details) 6 7""" 8Client widget for the IPython Console 9 10This is the widget used on all its tabs 11""" 12 13# Standard library imports 14from __future__ import absolute_import # Fix for Issue 1356 15 16import codecs 17import os 18import os.path as osp 19from string import Template 20from threading import Thread 21from time import ctime, time, strftime, gmtime 22 23# Third party imports (qtpy) 24from qtpy.QtCore import QUrl, QTimer, Signal, Slot 25from qtpy.QtGui import QKeySequence 26from qtpy.QtWidgets import (QHBoxLayout, QLabel, QMenu, QMessageBox, 27 QToolButton, QVBoxLayout, QWidget) 28 29# Local imports 30from spyder.config.base import _, get_image_path, get_module_source_path 31from spyder.config.gui import get_font, get_shortcut 32from spyder.utils import icon_manager as ima 33from spyder.utils import sourcecode 34from spyder.utils.encoding import get_coding 35from spyder.utils.environ import RemoteEnvDialog 36from spyder.utils.ipython.style import create_qss_style 37from spyder.utils.programs import TEMPDIR 38from spyder.utils.qthelpers import (add_actions, create_action, 39 create_toolbutton, DialogManager, 40 MENU_SEPARATOR) 41from spyder.py3compat import to_text_string 42from spyder.widgets.browser import WebView 43from spyder.widgets.ipythonconsole import ShellWidget 44from spyder.widgets.mixins import SaveHistoryMixin 45from spyder.widgets.variableexplorer.collectionseditor import CollectionsEditor 46 47 48#----------------------------------------------------------------------------- 49# Templates 50#----------------------------------------------------------------------------- 51# Using the same css file from the Help plugin for now. Maybe 52# later it'll be a good idea to create a new one. 53UTILS_PATH = get_module_source_path('spyder', 'utils') 54CSS_PATH = osp.join(UTILS_PATH, 'help', 'static', 'css') 55TEMPLATES_PATH = osp.join(UTILS_PATH, 'ipython', 'templates') 56 57BLANK = open(osp.join(TEMPLATES_PATH, 'blank.html')).read() 58LOADING = open(osp.join(TEMPLATES_PATH, 'loading.html')).read() 59KERNEL_ERROR = open(osp.join(TEMPLATES_PATH, 'kernel_error.html')).read() 60 61 62#----------------------------------------------------------------------------- 63# Auxiliary functions 64#----------------------------------------------------------------------------- 65def background(f): 66 """ 67 Call a function in a simple thread, to prevent blocking 68 69 Taken from the Jupyter Qtconsole project 70 """ 71 t = Thread(target=f) 72 t.start() 73 return t 74 75 76#----------------------------------------------------------------------------- 77# Client widget 78#----------------------------------------------------------------------------- 79class ClientWidget(QWidget, SaveHistoryMixin): 80 """ 81 Client widget for the IPython Console 82 83 This is a widget composed of a shell widget and a WebView info widget 84 to print different messages there. 85 """ 86 87 SEPARATOR = '{0}## ---({1})---'.format(os.linesep*2, ctime()) 88 INITHISTORY = ['# -*- coding: utf-8 -*-', 89 '# *** Spyder Python Console History Log ***',] 90 91 append_to_history = Signal(str, str) 92 93 def __init__(self, plugin, id_, 94 history_filename, config_options, 95 additional_options, interpreter_versions, 96 connection_file=None, hostname=None, 97 menu_actions=None, slave=False, 98 external_kernel=False, given_name=None, 99 show_elapsed_time=False, 100 reset_warning=True): 101 super(ClientWidget, self).__init__(plugin) 102 SaveHistoryMixin.__init__(self, history_filename) 103 104 # --- Init attrs 105 self.id_ = id_ 106 self.connection_file = connection_file 107 self.hostname = hostname 108 self.menu_actions = menu_actions 109 self.slave = slave 110 self.external_kernel = external_kernel 111 self.given_name = given_name 112 self.show_elapsed_time = show_elapsed_time 113 self.reset_warning = reset_warning 114 115 # --- Other attrs 116 self.options_button = None 117 self.stop_button = None 118 self.reset_button = None 119 self.stop_icon = ima.icon('stop') 120 self.history = [] 121 self.allow_rename = True 122 self.stderr_dir = None 123 124 # --- Widgets 125 self.shellwidget = ShellWidget(config=config_options, 126 ipyclient=self, 127 additional_options=additional_options, 128 interpreter_versions=interpreter_versions, 129 external_kernel=external_kernel, 130 local_kernel=True) 131 self.infowidget = WebView(self) 132 self.set_infowidget_font() 133 self.loading_page = self._create_loading_page() 134 self._show_loading_page() 135 136 # Elapsed time 137 self.time_label = None 138 self.t0 = None 139 self.timer = QTimer(self) 140 141 # --- Layout 142 vlayout = QVBoxLayout() 143 toolbar_buttons = self.get_toolbar_buttons() 144 145 hlayout = QHBoxLayout() 146 hlayout.addWidget(self.create_time_label()) 147 hlayout.addStretch(0) 148 for button in toolbar_buttons: 149 hlayout.addWidget(button) 150 151 vlayout.addLayout(hlayout) 152 vlayout.setContentsMargins(0, 0, 0, 0) 153 vlayout.addWidget(self.shellwidget) 154 vlayout.addWidget(self.infowidget) 155 self.setLayout(vlayout) 156 157 # --- Exit function 158 self.exit_callback = lambda: plugin.close_client(client=self) 159 160 # --- Dialog manager 161 self.dialog_manager = DialogManager() 162 163 # Show timer 164 self.update_time_label_visibility() 165 166 #------ Public API -------------------------------------------------------- 167 @property 168 def kernel_id(self): 169 """Get kernel id""" 170 if self.connection_file is not None: 171 json_file = osp.basename(self.connection_file) 172 return json_file.split('.json')[0] 173 174 @property 175 def stderr_file(self): 176 """Filename to save kernel stderr output.""" 177 stderr_file = None 178 if self.connection_file is not None: 179 stderr_file = self.kernel_id + '.stderr' 180 if self.stderr_dir is not None: 181 stderr_file = osp.join(self.stderr_dir, stderr_file) 182 else: 183 try: 184 if not osp.isdir(TEMPDIR): 185 os.makedirs(TEMPDIR) 186 stderr_file = osp.join(TEMPDIR, stderr_file) 187 except (IOError, OSError): 188 stderr_file = None 189 return stderr_file 190 191 def configure_shellwidget(self, give_focus=True): 192 """Configure shellwidget after kernel is started""" 193 if give_focus: 194 self.get_control().setFocus() 195 196 # Set exit callback 197 self.shellwidget.set_exit_callback() 198 199 # To save history 200 self.shellwidget.executing.connect(self.add_to_history) 201 202 # For Mayavi to run correctly 203 self.shellwidget.executing.connect( 204 self.shellwidget.set_backend_for_mayavi) 205 206 # To update history after execution 207 self.shellwidget.executed.connect(self.update_history) 208 209 # To update the Variable Explorer after execution 210 self.shellwidget.executed.connect( 211 self.shellwidget.refresh_namespacebrowser) 212 213 # To enable the stop button when executing a process 214 self.shellwidget.executing.connect(self.enable_stop_button) 215 216 # To disable the stop button after execution stopped 217 self.shellwidget.executed.connect(self.disable_stop_button) 218 219 # To show kernel restarted/died messages 220 self.shellwidget.sig_kernel_restarted.connect( 221 self.kernel_restarted_message) 222 223 # To correctly change Matplotlib backend interactively 224 self.shellwidget.executing.connect( 225 self.shellwidget.change_mpl_backend) 226 227 # To show env and sys.path contents 228 self.shellwidget.sig_show_syspath.connect(self.show_syspath) 229 self.shellwidget.sig_show_env.connect(self.show_env) 230 231 # To sync with working directory toolbar 232 self.shellwidget.executed.connect(self.shellwidget.get_cwd) 233 234 # To apply style 235 self.set_color_scheme(self.shellwidget.syntax_style, reset=False) 236 237 # To hide the loading page 238 self.shellwidget.sig_prompt_ready.connect(self._hide_loading_page) 239 240 # Show possible errors when setting Matplotlib backend 241 self.shellwidget.sig_prompt_ready.connect( 242 self._show_mpl_backend_errors) 243 244 def enable_stop_button(self): 245 self.stop_button.setEnabled(True) 246 247 def disable_stop_button(self): 248 self.stop_button.setDisabled(True) 249 250 @Slot() 251 def stop_button_click_handler(self): 252 """Method to handle what to do when the stop button is pressed""" 253 self.stop_button.setDisabled(True) 254 # Interrupt computations or stop debugging 255 if not self.shellwidget._reading: 256 self.interrupt_kernel() 257 else: 258 self.shellwidget.write_to_stdin('exit') 259 260 def show_kernel_error(self, error): 261 """Show kernel initialization errors in infowidget.""" 262 # Replace end of line chars with <br> 263 eol = sourcecode.get_eol_chars(error) 264 if eol: 265 error = error.replace(eol, '<br>') 266 267 # Don't break lines in hyphens 268 # From http://stackoverflow.com/q/7691569/438386 269 error = error.replace('-', '‑') 270 271 # Create error page 272 message = _("An error ocurred while starting the kernel") 273 kernel_error_template = Template(KERNEL_ERROR) 274 page = kernel_error_template.substitute(css_path=CSS_PATH, 275 message=message, 276 error=error) 277 278 # Show error 279 self.infowidget.setHtml(page) 280 self.shellwidget.hide() 281 self.infowidget.show() 282 283 def get_name(self): 284 """Return client name""" 285 if self.given_name is None: 286 # Name according to host 287 if self.hostname is None: 288 name = _("Console") 289 else: 290 name = self.hostname 291 # Adding id to name 292 client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] 293 name = name + u' ' + client_id 294 else: 295 name = self.given_name + u'/' + self.id_['str_id'] 296 return name 297 298 def get_control(self): 299 """Return the text widget (or similar) to give focus to""" 300 # page_control is the widget used for paging 301 page_control = self.shellwidget._page_control 302 if page_control and page_control.isVisible(): 303 return page_control 304 else: 305 return self.shellwidget._control 306 307 def get_kernel(self): 308 """Get kernel associated with this client""" 309 return self.shellwidget.kernel_manager 310 311 def get_options_menu(self): 312 """Return options menu""" 313 reset_action = create_action(self, _("Remove all variables"), 314 icon=ima.icon('editdelete'), 315 triggered=self.reset_namespace) 316 317 self.show_time_action = create_action(self, _("Show elapsed time"), 318 toggled=self.set_elapsed_time_visible) 319 320 env_action = create_action( 321 self, 322 _("Show environment variables"), 323 icon=ima.icon('environ'), 324 triggered=self.shellwidget.get_env 325 ) 326 327 syspath_action = create_action( 328 self, 329 _("Show sys.path contents"), 330 icon=ima.icon('syspath'), 331 triggered=self.shellwidget.get_syspath 332 ) 333 334 self.show_time_action.setChecked(self.show_elapsed_time) 335 additional_actions = [reset_action, 336 MENU_SEPARATOR, 337 env_action, 338 syspath_action, 339 self.show_time_action] 340 341 if self.menu_actions is not None: 342 return self.menu_actions + additional_actions 343 else: 344 return additional_actions 345 346 def get_toolbar_buttons(self): 347 """Return toolbar buttons list.""" 348 buttons = [] 349 350 # Code to add the stop button 351 if self.stop_button is None: 352 self.stop_button = create_toolbutton( 353 self, 354 text=_("Stop"), 355 icon=self.stop_icon, 356 tip=_("Stop the current command")) 357 self.disable_stop_button() 358 # set click event handler 359 self.stop_button.clicked.connect(self.stop_button_click_handler) 360 if self.stop_button is not None: 361 buttons.append(self.stop_button) 362 363 # Reset namespace button 364 if self.reset_button is None: 365 self.reset_button = create_toolbutton( 366 self, 367 text=_("Remove"), 368 icon=ima.icon('editdelete'), 369 tip=_("Remove all variables"), 370 triggered=self.reset_namespace) 371 if self.reset_button is not None: 372 buttons.append(self.reset_button) 373 374 if self.options_button is None: 375 options = self.get_options_menu() 376 if options: 377 self.options_button = create_toolbutton(self, 378 text=_('Options'), icon=ima.icon('tooloptions')) 379 self.options_button.setPopupMode(QToolButton.InstantPopup) 380 menu = QMenu(self) 381 add_actions(menu, options) 382 self.options_button.setMenu(menu) 383 if self.options_button is not None: 384 buttons.append(self.options_button) 385 386 return buttons 387 388 def add_actions_to_context_menu(self, menu): 389 """Add actions to IPython widget context menu""" 390 inspect_action = create_action(self, _("Inspect current object"), 391 QKeySequence(get_shortcut('console', 392 'inspect current object')), 393 icon=ima.icon('MessageBoxInformation'), 394 triggered=self.inspect_object) 395 396 clear_line_action = create_action(self, _("Clear line or block"), 397 QKeySequence(get_shortcut( 398 'console', 399 'clear line')), 400 triggered=self.clear_line) 401 402 reset_namespace_action = create_action(self, _("Remove all variables"), 403 QKeySequence(get_shortcut( 404 'ipython_console', 405 'reset namespace')), 406 icon=ima.icon('editdelete'), 407 triggered=self.reset_namespace) 408 409 clear_console_action = create_action(self, _("Clear console"), 410 QKeySequence(get_shortcut('console', 411 'clear shell')), 412 triggered=self.clear_console) 413 414 quit_action = create_action(self, _("&Quit"), icon=ima.icon('exit'), 415 triggered=self.exit_callback) 416 417 add_actions(menu, (None, inspect_action, clear_line_action, 418 clear_console_action, reset_namespace_action, 419 None, quit_action)) 420 return menu 421 422 def set_font(self, font): 423 """Set IPython widget's font""" 424 self.shellwidget._control.setFont(font) 425 self.shellwidget.font = font 426 427 def set_infowidget_font(self): 428 """Set font for infowidget""" 429 font = get_font(option='rich_font') 430 self.infowidget.set_font(font) 431 432 def set_color_scheme(self, color_scheme, reset=True): 433 """Set IPython color scheme.""" 434 self.shellwidget.set_color_scheme(color_scheme, reset) 435 436 def shutdown(self): 437 """Shutdown kernel""" 438 if self.get_kernel() is not None and not self.slave: 439 self.shellwidget.kernel_manager.shutdown_kernel() 440 if self.shellwidget.kernel_client is not None: 441 background(self.shellwidget.kernel_client.stop_channels) 442 443 def interrupt_kernel(self): 444 """Interrupt the associanted Spyder kernel if it's running""" 445 # Needed to prevent a crash when a kernel is not running. 446 # See issue 6299 447 try: 448 self.shellwidget.request_interrupt_kernel() 449 except RuntimeError: 450 pass 451 452 @Slot() 453 def restart_kernel(self): 454 """ 455 Restart the associated kernel. 456 457 Took this code from the qtconsole project 458 Licensed under the BSD license 459 """ 460 sw = self.shellwidget 461 462 message = _('Are you sure you want to restart the kernel?') 463 buttons = QMessageBox.Yes | QMessageBox.No 464 result = QMessageBox.question(self, _('Restart kernel?'), 465 message, buttons) 466 467 if result == QMessageBox.Yes: 468 if sw.kernel_manager: 469 if self.infowidget.isVisible(): 470 self.infowidget.hide() 471 sw.show() 472 try: 473 sw.kernel_manager.restart_kernel() 474 except RuntimeError as e: 475 sw._append_plain_text( 476 _('Error restarting kernel: %s\n') % e, 477 before_prompt=True 478 ) 479 else: 480 # For issue 6235. IPython was changing the setting of 481 # %colors on windows by assuming it was using a dark 482 # background. This corrects it based on the scheme. 483 self.set_color_scheme(sw.syntax_style) 484 sw._append_html(_("<br>Restarting kernel...\n<hr><br>"), 485 before_prompt=False) 486 else: 487 sw._append_plain_text( 488 _('Cannot restart a kernel not started by Spyder\n'), 489 before_prompt=True 490 ) 491 492 @Slot(str) 493 def kernel_restarted_message(self, msg): 494 """Show kernel restarted/died messages.""" 495 try: 496 stderr = codecs.open(self.stderr_file, 'r', 497 encoding='utf-8').read() 498 except UnicodeDecodeError: 499 # This is needed since the stderr file could be encoded 500 # in something different to utf-8. 501 # See issue 4191 502 try: 503 stderr = self._read_stderr() 504 except: 505 stderr = None 506 except (OSError, IOError): 507 stderr = None 508 509 if stderr: 510 self.show_kernel_error('<tt>%s</tt>' % stderr) 511 else: 512 self.shellwidget._append_html("<br>%s<hr><br>" % msg, 513 before_prompt=False) 514 515 516 @Slot() 517 def inspect_object(self): 518 """Show how to inspect an object with our Help plugin""" 519 self.shellwidget._control.inspect_current_object() 520 521 @Slot() 522 def clear_line(self): 523 """Clear a console line""" 524 self.shellwidget._keyboard_quit() 525 526 @Slot() 527 def clear_console(self): 528 """Clear the whole console""" 529 self.shellwidget.clear_console() 530 531 @Slot() 532 def reset_namespace(self): 533 """Resets the namespace by removing all names defined by the user""" 534 self.shellwidget.reset_namespace(warning=self.reset_warning, 535 message=True) 536 537 def update_history(self): 538 self.history = self.shellwidget._history 539 540 @Slot(object) 541 def show_syspath(self, syspath): 542 """Show sys.path contents.""" 543 if syspath is not None: 544 editor = CollectionsEditor() 545 editor.setup(syspath, title="sys.path contents", readonly=True, 546 width=600, icon=ima.icon('syspath')) 547 self.dialog_manager.show(editor) 548 else: 549 return 550 551 @Slot(object) 552 def show_env(self, env): 553 """Show environment variables.""" 554 self.dialog_manager.show(RemoteEnvDialog(env)) 555 556 def create_time_label(self): 557 """Create elapsed time label widget (if necessary) and return it""" 558 if self.time_label is None: 559 self.time_label = QLabel() 560 return self.time_label 561 562 def show_time(self, end=False): 563 """Text to show in time_label.""" 564 if self.time_label is None: 565 return 566 elapsed_time = time() - self.t0 567 if elapsed_time > 24 * 3600: # More than a day...! 568 fmt = "%d %H:%M:%S" 569 else: 570 fmt = "%H:%M:%S" 571 if end: 572 color = "#AAAAAA" 573 else: 574 color = "#AA6655" 575 text = "<span style=\'color: %s\'><b>%s" \ 576 "</b></span>" % (color, strftime(fmt, gmtime(elapsed_time))) 577 self.time_label.setText(text) 578 579 def update_time_label_visibility(self): 580 """Update elapsed time visibility.""" 581 self.time_label.setVisible(self.show_elapsed_time) 582 583 @Slot(bool) 584 def set_elapsed_time_visible(self, state): 585 """Slot to show/hide elapsed time label.""" 586 self.show_elapsed_time = state 587 if self.time_label is not None: 588 self.time_label.setVisible(state) 589 590 #------ Private API ------------------------------------------------------- 591 def _create_loading_page(self): 592 """Create html page to show while the kernel is starting""" 593 loading_template = Template(LOADING) 594 loading_img = get_image_path('loading_sprites.png') 595 if os.name == 'nt': 596 loading_img = loading_img.replace('\\', '/') 597 message = _("Connecting to kernel...") 598 page = loading_template.substitute(css_path=CSS_PATH, 599 loading_img=loading_img, 600 message=message) 601 return page 602 603 def _show_loading_page(self): 604 """Show animation while the kernel is loading.""" 605 self.shellwidget.hide() 606 self.infowidget.show() 607 self.infowidget.setHtml(self.loading_page, 608 QUrl.fromLocalFile(CSS_PATH)) 609 610 def _hide_loading_page(self): 611 """Hide animation shown while the kernel is loading.""" 612 self.infowidget.hide() 613 self.shellwidget.show() 614 self.infowidget.setHtml(BLANK) 615 self.shellwidget.sig_prompt_ready.disconnect(self._hide_loading_page) 616 617 def _read_stderr(self): 618 """Read the stderr file of the kernel.""" 619 stderr_text = open(self.stderr_file, 'rb').read() 620 encoding = get_coding(stderr_text) 621 stderr = to_text_string(stderr_text, encoding) 622 return stderr 623 624 def _show_mpl_backend_errors(self): 625 """ 626 Show possible errors when setting the selected Matplotlib backend. 627 """ 628 if not self.external_kernel: 629 self.shellwidget.silent_execute( 630 "get_ipython().kernel._show_mpl_backend_errors()") 631 self.shellwidget.sig_prompt_ready.disconnect( 632 self._show_mpl_backend_errors) 633