class ClientWidget(QWidget, SaveHistoryMixin): """ Client widget for the IPython Console This widget is necessary to handle the interaction between the plugin and each shell widget. """ SEPARATOR = '{0}## ---({1})---'.format(os.linesep * 2, time.ctime()) INITHISTORY = [ '# -*- coding: utf-8 -*-', '# *** Spyder Python Console History Log ***', ] append_to_history = Signal(str, str) def __init__(self, plugin, id_, history_filename, config_options, additional_options, interpreter_versions, connection_file=None, hostname=None, menu_actions=None, slave=False, external_kernel=False, given_name=None, options_button=None, show_elapsed_time=False, reset_warning=True, ask_before_restart=True, ask_before_closing=False, css_path=None): super(ClientWidget, self).__init__(plugin) SaveHistoryMixin.__init__(self, history_filename) # --- Init attrs self.plugin = plugin self.id_ = id_ self.connection_file = connection_file self.hostname = hostname self.menu_actions = menu_actions self.slave = slave self.external_kernel = external_kernel self.given_name = given_name self.show_elapsed_time = show_elapsed_time self.reset_warning = reset_warning self.ask_before_restart = ask_before_restart self.ask_before_closing = ask_before_closing # --- Other attrs self.options_button = options_button self.stop_button = None self.reset_button = None self.stop_icon = ima.icon('stop') self.history = [] self.allow_rename = True self.stderr_dir = None self.is_error_shown = False self.restart_thread = None self.give_focus = True if css_path is None: self.css_path = CSS_PATH else: self.css_path = css_path # --- Widgets self.shellwidget = ShellWidget( config=config_options, ipyclient=self, additional_options=additional_options, interpreter_versions=interpreter_versions, external_kernel=external_kernel, local_kernel=True) self.infowidget = plugin.infowidget self.blank_page = self._create_blank_page() self.loading_page = self._create_loading_page() # To keep a reference to the page to be displayed # in infowidget self.info_page = None self._before_prompt_is_ready() # Elapsed time self.time_label = None self.t0 = time.monotonic() self.timer = QTimer(self) self.show_time_action = create_action( self, _("Show elapsed time"), toggled=self.set_elapsed_time_visible) # --- Layout self.layout = QVBoxLayout() toolbar_buttons = self.get_toolbar_buttons() hlayout = QHBoxLayout() hlayout.addWidget(self.create_time_label()) hlayout.addStretch(0) for button in toolbar_buttons: hlayout.addWidget(button) self.layout.addLayout(hlayout) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.shellwidget) self.layout.addWidget(self.infowidget) self.setLayout(self.layout) # --- Exit function self.exit_callback = lambda: plugin.close_client(client=self) # --- Dialog manager self.dialog_manager = DialogManager() # Show timer self.update_time_label_visibility() # Poll for stderr changes self.stderr_mtime = 0 self.stderr_timer = QTimer(self) self.stderr_timer.timeout.connect(self.poll_stderr_file_change) self.stderr_timer.setInterval(1000) self.stderr_timer.start() def __del__(self): """Close threads to avoid segfault""" if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.wait() #------ Public API -------------------------------------------------------- @property def kernel_id(self): """Get kernel id""" if self.connection_file is not None: json_file = osp.basename(self.connection_file) return json_file.split('.json')[0] @property def stderr_file(self): """Filename to save kernel stderr output.""" stderr_file = None if self.connection_file is not None: stderr_file = self.kernel_id + '.stderr' if self.stderr_dir is not None: stderr_file = osp.join(self.stderr_dir, stderr_file) else: try: stderr_file = osp.join(get_temp_dir(), stderr_file) except (IOError, OSError): stderr_file = None return stderr_file @property def stderr_handle(self): """Get handle to stderr_file.""" if self.stderr_file is not None: # Needed to prevent any error that could appear. # See spyder-ide/spyder#6267. try: handle = codecs.open(self.stderr_file, 'w', encoding='utf-8') except Exception: handle = None else: handle = None return handle def remove_stderr_file(self): """Remove stderr_file associated with the client.""" try: # Defer closing the stderr_handle until the client # is closed because jupyter_client needs it open # while it tries to restart the kernel self.stderr_handle.close() os.remove(self.stderr_file) except Exception: pass def get_stderr_contents(self): """Get the contents of the stderr kernel file.""" try: stderr = self._read_stderr() except Exception: stderr = None return stderr @Slot() def poll_stderr_file_change(self): """Check if the stderr file just changed""" try: mtime = os.stat(self.stderr_file).st_mtime except Exception: return if mtime == self.stderr_mtime: return self.stderr_mtime = mtime stderr = self.get_stderr_contents() if stderr: self.shellwidget._append_plain_text('\n' + stderr, before_prompt=True) def configure_shellwidget(self, give_focus=True): """Configure shellwidget after kernel is connected.""" self.give_focus = give_focus # Make sure the kernel sends the comm config over self.shellwidget.call_kernel()._send_comm_config() # Set exit callback self.shellwidget.set_exit_callback() # To save history self.shellwidget.executing.connect(self.add_to_history) # For Mayavi to run correctly self.shellwidget.executing.connect( self.shellwidget.set_backend_for_mayavi) # To update history after execution self.shellwidget.executed.connect(self.update_history) # To update the Variable Explorer after execution self.shellwidget.executed.connect( self.shellwidget.refresh_namespacebrowser) # To enable the stop button when executing a process self.shellwidget.executing.connect(self.enable_stop_button) # To disable the stop button after execution stopped self.shellwidget.executed.connect(self.disable_stop_button) # To show kernel restarted/died messages self.shellwidget.sig_kernel_restarted_message.connect( self.kernel_restarted_message) self.shellwidget.sig_kernel_restarted.connect(self._finalise_restart) # To correctly change Matplotlib backend interactively self.shellwidget.executing.connect(self.shellwidget.change_mpl_backend) # To show env and sys.path contents self.shellwidget.sig_show_syspath.connect(self.show_syspath) self.shellwidget.sig_show_env.connect(self.show_env) # To sync with working directory toolbar self.shellwidget.executed.connect(self.shellwidget.update_cwd) # To apply style self.set_color_scheme(self.shellwidget.syntax_style, reset=False) def add_to_history(self, command): """Add command to history""" if self.shellwidget.is_debugging(): return return super(ClientWidget, self).add_to_history(command) def _before_prompt_is_ready(self): """Configure shellwidget before kernel is connected.""" self._show_loading_page() self.shellwidget.sig_prompt_ready.connect(self._when_prompt_is_ready) # If remote execution, the loading page should be hidden as well self.shellwidget.sig_remote_execute.connect(self._when_prompt_is_ready) def _when_prompt_is_ready(self): """Configuration after the prompt is shown.""" # To hide the loading page self._hide_loading_page() # Show possible errors when setting Matplotlib backend self._show_mpl_backend_errors() # To show if special console is valid self._check_special_console_error() self.shellwidget.sig_prompt_ready.disconnect( self._when_prompt_is_ready) self.shellwidget.sig_remote_execute.disconnect( self._when_prompt_is_ready) # It's necessary to do this at this point to avoid giving # focus to _control at startup. self._connect_control_signals() if self.give_focus: self.shellwidget._control.setFocus() def enable_stop_button(self): self.stop_button.setEnabled(True) def disable_stop_button(self): # This avoids disabling automatically the button when # re-running files on dedicated consoles. # See spyder-ide/spyder#5958. if not self.shellwidget._executing: # This avoids disabling the button while debugging # see spyder-ide/spyder#13283 if not self.shellwidget.is_waiting_pdb_input(): self.stop_button.setDisabled(True) else: self.stop_button.setEnabled(True) @Slot() def stop_button_click_handler(self): """Method to handle what to do when the stop button is pressed""" self.stop_button.setDisabled(True) # Interrupt computations or stop debugging if not self.shellwidget.is_waiting_pdb_input(): self.interrupt_kernel() else: self.shellwidget.pdb_execute_command('exit') def show_kernel_error(self, error): """Show kernel initialization errors in infowidget.""" # Replace end of line chars with <br> eol = sourcecode.get_eol_chars(error) if eol: error = error.replace(eol, '<br>') # Don't break lines in hyphens # From https://stackoverflow.com/q/7691569/438386 error = error.replace('-', '‑') # Create error page message = _("An error ocurred while starting the kernel") kernel_error_template = Template(KERNEL_ERROR) self.info_page = kernel_error_template.substitute( css_path=self.css_path, message=message, error=error) # Show error self.set_info_page() self.shellwidget.hide() self.infowidget.show() # Tell the client we're in error mode self.is_error_shown = True def get_name(self): """Return client name""" if self.given_name is None: # Name according to host if self.hostname is None: name = _("Console") else: name = self.hostname # Adding id to name client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] name = name + u' ' + client_id elif self.given_name in ["Pylab", "SymPy", "Cython"]: client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] name = self.given_name + u' ' + client_id else: name = self.given_name + u'/' + self.id_['str_id'] return name def get_control(self): """Return the text widget (or similar) to give focus to""" # page_control is the widget used for paging page_control = self.shellwidget._page_control if page_control and page_control.isVisible(): return page_control else: return self.shellwidget._control def get_kernel(self): """Get kernel associated with this client""" return self.shellwidget.kernel_manager def get_options_menu(self): """Return options menu""" env_action = create_action(self, _("Show environment variables"), icon=ima.icon('environ'), triggered=self.shellwidget.request_env) syspath_action = create_action( self, _("Show sys.path contents"), icon=ima.icon('syspath'), triggered=self.shellwidget.request_syspath) self.show_time_action.setChecked(self.show_elapsed_time) additional_actions = [ MENU_SEPARATOR, env_action, syspath_action, self.show_time_action ] if self.menu_actions is not None: console_menu = self.menu_actions + additional_actions return console_menu else: return additional_actions def get_toolbar_buttons(self): """Return toolbar buttons list.""" buttons = [] # Code to add the stop button if self.stop_button is None: self.stop_button = create_toolbutton( self, text=_("Stop"), icon=self.stop_icon, tip=_("Stop the current command")) self.disable_stop_button() # set click event handler self.stop_button.clicked.connect(self.stop_button_click_handler) if self.stop_button is not None: buttons.append(self.stop_button) # Reset namespace button if self.reset_button is None: self.reset_button = create_toolbutton( self, text=_("Remove"), icon=ima.icon('editdelete'), tip=_("Remove all variables"), triggered=self.reset_namespace) if self.reset_button is not None: buttons.append(self.reset_button) if self.options_button is None: options = self.get_options_menu() if options: self.options_button = create_toolbutton( self, text=_('Options'), icon=ima.icon('tooloptions')) self.options_button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self) add_actions(menu, options) self.options_button.setMenu(menu) if self.options_button is not None: buttons.append(self.options_button) return buttons def add_actions_to_context_menu(self, menu): """Add actions to IPython widget context menu""" inspect_action = create_action( self, _("Inspect current object"), QKeySequence(CONF.get_shortcut('console', 'inspect current object')), icon=ima.icon('MessageBoxInformation'), triggered=self.inspect_object) clear_line_action = create_action( self, _("Clear line or block"), QKeySequence(CONF.get_shortcut('console', 'clear line')), triggered=self.clear_line) reset_namespace_action = create_action(self, _("Remove all variables"), QKeySequence( CONF.get_shortcut( 'ipython_console', 'reset namespace')), icon=ima.icon('editdelete'), triggered=self.reset_namespace) clear_console_action = create_action( self, _("Clear console"), QKeySequence(CONF.get_shortcut('console', 'clear shell')), triggered=self.clear_console) quit_action = create_action(self, _("&Quit"), icon=ima.icon('exit'), triggered=self.exit_callback) add_actions( menu, (None, inspect_action, clear_line_action, clear_console_action, reset_namespace_action, None, quit_action)) return menu def set_font(self, font): """Set IPython widget's font""" self.shellwidget._control.setFont(font) self.shellwidget.font = font def set_color_scheme(self, color_scheme, reset=True): """Set IPython color scheme.""" # Needed to handle not initialized kernel_client # See spyder-ide/spyder#6996. try: self.shellwidget.set_color_scheme(color_scheme, reset) except AttributeError: pass def shutdown(self): """Shutdown kernel""" if self.get_kernel() is not None and not self.slave: self.shellwidget.shutdown() def close(self): """Close client""" self.shellwidget.will_close(self.get_kernel() is None or self.slave) super(ClientWidget, self).close() def interrupt_kernel(self): """Interrupt the associanted Spyder kernel if it's running""" # Needed to prevent a crash when a kernel is not running. # See spyder-ide/spyder#6299. try: self.shellwidget.request_interrupt_kernel() except RuntimeError: pass @Slot() def restart_kernel(self): """ Restart the associated kernel. Took this code from the qtconsole project Licensed under the BSD license """ sw = self.shellwidget if not running_under_pytest() and self.ask_before_restart: message = _('Are you sure you want to restart the kernel?') buttons = QMessageBox.Yes | QMessageBox.No result = QMessageBox.question(self, _('Restart kernel?'), message, buttons) else: result = None if (result == QMessageBox.Yes or running_under_pytest() or not self.ask_before_restart): if sw.kernel_manager: if self.infowidget.isVisible(): self.infowidget.hide() if self._abort_kernel_restart(): sw.spyder_kernel_comm.close() return self._show_loading_page() # Close comm sw.spyder_kernel_comm.close() # Stop autorestart mechanism sw.kernel_manager.stop_restarter() sw.kernel_manager.autorestart = False # Create and run restarting thread if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.finished.disconnect() self.restart_thread.terminate() self.restart_thread.wait() self.restart_thread = QThread() self.restart_thread.run = self._restart_thread_main self.restart_thread.error = None self.restart_thread.finished.connect( lambda: self._finalise_restart(True)) self.restart_thread.start() else: sw._append_plain_text( _('Cannot restart a kernel not started by Spyder\n'), before_prompt=True) self._hide_loading_page() def _restart_thread_main(self): """Restart the kernel in a thread.""" try: self.shellwidget.kernel_manager.restart_kernel( stderr=self.stderr_handle) except RuntimeError as e: self.restart_thread.error = e def _finalise_restart(self, reset=False): """Finishes the restarting of the kernel.""" sw = self.shellwidget if self._abort_kernel_restart(): sw.spyder_kernel_comm.close() return if self.restart_thread and self.restart_thread.error is not None: sw._append_plain_text(_('Error restarting kernel: %s\n') % self.restart_thread.error, before_prompt=True) else: # Reset Pdb state and reopen comm sw._pdb_in_loop = False sw.spyder_kernel_comm.remove() sw.spyder_kernel_comm.open_comm(sw.kernel_client) # Start autorestart mechanism sw.kernel_manager.autorestart = True sw.kernel_manager.start_restarter() # For spyder-ide/spyder#6235, IPython was changing the # setting of %colors on windows by assuming it was using a # dark background. This corrects it based on the scheme. self.set_color_scheme(sw.syntax_style, reset=reset) sw._append_html(_("<br>Restarting kernel...\n<hr><br>"), before_prompt=True) self._hide_loading_page() self.stop_button.setDisabled(True) self.restart_thread = None @Slot(str) def kernel_restarted_message(self, msg): """Show kernel restarted/died messages.""" if not self.is_error_shown: # If there are kernel creation errors, jupyter_client will # try to restart the kernel and qtconsole prints a # message about it. # So we read the kernel's stderr_file and display its # contents in the client instead of the usual message shown # by qtconsole. stderr = self.get_stderr_contents() if stderr: self.show_kernel_error('<tt>%s</tt>' % stderr) else: self.shellwidget._append_html("<br>%s<hr><br>" % msg, before_prompt=False) @Slot() def inspect_object(self): """Show how to inspect an object with our Help plugin""" self.shellwidget._control.inspect_current_object() @Slot() def clear_line(self): """Clear a console line""" self.shellwidget._keyboard_quit() @Slot() def clear_console(self): """Clear the whole console""" self.shellwidget.clear_console() @Slot() def reset_namespace(self): """Resets the namespace by removing all names defined by the user""" self.shellwidget.reset_namespace(warning=self.reset_warning, message=True) def update_history(self): self.history = self.shellwidget._history @Slot(object) def show_syspath(self, syspath): """Show sys.path contents.""" if syspath is not None: editor = CollectionsEditor(self) editor.setup(syspath, title="sys.path contents", readonly=True, icon=ima.icon('syspath')) self.dialog_manager.show(editor) else: return @Slot(object) def show_env(self, env): """Show environment variables.""" self.dialog_manager.show(RemoteEnvDialog(env, parent=self)) def create_time_label(self): """Create elapsed time label widget (if necessary) and return it""" if self.time_label is None: self.time_label = QLabel() return self.time_label def show_time(self, end=False): """Text to show in time_label.""" if self.time_label is None: return elapsed_time = time.monotonic() - self.t0 # System time changed to past date, so reset start. if elapsed_time < 0: self.t0 = time.monotonic() elapsed_time = 0 if elapsed_time > 24 * 3600: # More than a day...! fmt = "%d %H:%M:%S" else: fmt = "%H:%M:%S" if end: color = QStylePalette.COLOR_TEXT_3 else: color = QStylePalette.COLOR_ACCENT_4 text = "<span style=\'color: %s\'><b>%s" \ "</b></span>" % (color, time.strftime(fmt, time.gmtime(elapsed_time))) self.time_label.setText(text) def update_time_label_visibility(self): """Update elapsed time visibility.""" self.time_label.setVisible(self.show_elapsed_time) @Slot(bool) def set_elapsed_time_visible(self, state): """Slot to show/hide elapsed time label.""" self.show_elapsed_time = state if self.time_label is not None: self.time_label.setVisible(state) def set_info_page(self): """Set current info_page.""" if self.info_page is not None: self.infowidget.setHtml(self.info_page, QUrl.fromLocalFile(self.css_path)) #------ Private API ------------------------------------------------------- def _create_loading_page(self): """Create html page to show while the kernel is starting""" loading_template = Template(LOADING) loading_img = get_image_path('loading_sprites') if os.name == 'nt': loading_img = loading_img.replace('\\', '/') message = _("Connecting to kernel...") page = loading_template.substitute(css_path=self.css_path, loading_img=loading_img, message=message) return page def _create_blank_page(self): """Create html page to show while the kernel is starting""" loading_template = Template(BLANK) page = loading_template.substitute(css_path=self.css_path) return page def _show_loading_page(self): """Show animation while the kernel is loading.""" self.shellwidget.hide() self.infowidget.show() self.info_page = self.loading_page self.set_info_page() def _hide_loading_page(self): """Hide animation shown while the kernel is loading.""" self.infowidget.hide() self.info_page = self.blank_page self.set_info_page() self.shellwidget.show() def _read_stderr(self): """Read the stderr file of the kernel.""" # We need to read stderr_file as bytes to be able to # detect its encoding with chardet f = open(self.stderr_file, 'rb') try: stderr_text = f.read() # This is needed to avoid showing an empty error message # when the kernel takes too much time to start. # See spyder-ide/spyder#8581. if not stderr_text: return '' # This is needed since the stderr file could be encoded # in something different to utf-8. # See spyder-ide/spyder#4191. encoding = get_coding(stderr_text) stderr_text = to_text_string(stderr_text, encoding) return stderr_text finally: f.close() def _show_mpl_backend_errors(self): """ Show possible errors when setting the selected Matplotlib backend. """ if not self.external_kernel: self.shellwidget.call_kernel().show_mpl_backend_errors() def _check_special_console_error(self): """Check if the dependecies for special consoles are available.""" self.shellwidget.call_kernel(callback=self._show_special_console_error ).is_special_kernel_valid() def _show_special_console_error(self, missing_dependency): if missing_dependency is not None: error_message = _( "Your Python environment or installation doesn't have the " "<tt>{missing_dependency}</tt> module installed or it " "occurred a problem importing it. Due to that, it is not " "possible for Spyder to create this special console for " "you.").format(missing_dependency=missing_dependency) self.show_kernel_error(error_message) def _abort_kernel_restart(self): """ Abort kernel restart if there are errors while starting it. We also ignore errors about comms, which are irrelevant. """ stderr = self.get_stderr_contents() if stderr and 'No such comm' not in stderr: return True else: return False def _connect_control_signals(self): """Connect signals of control widgets.""" control = self.shellwidget._control page_control = self.shellwidget._page_control control.focus_changed.connect(lambda: self.plugin.focus_changed.emit()) page_control.focus_changed.connect( lambda: self.plugin.focus_changed.emit()) control.visibility_changed.connect(self.plugin.refresh_plugin) page_control.visibility_changed.connect(self.plugin.refresh_plugin) page_control.show_find_widget.connect(self.plugin.find_widget.show)
class ClientWidget(QWidget, SaveHistoryMixin, SpyderWidgetMixin): """ Client widget for the IPython Console This widget is necessary to handle the interaction between the plugin and each shell widget. """ sig_append_to_history_requested = Signal(str, str) sig_execution_state_changed = Signal() CONF_SECTION = 'ipython_console' SEPARATOR = '{0}## ---({1})---'.format(os.linesep*2, time.ctime()) INITHISTORY = ['# -*- coding: utf-8 -*-', '# *** Spyder Python Console History Log ***', ] def __init__(self, parent, id_, history_filename, config_options, additional_options, interpreter_versions, connection_file=None, hostname=None, context_menu_actions=(), menu_actions=None, is_external_kernel=False, is_spyder_kernel=True, given_name=None, give_focus=True, options_button=None, time_label=None, show_elapsed_time=False, reset_warning=True, ask_before_restart=True, ask_before_closing=False, css_path=None, handlers={}, stderr_obj=None, stdout_obj=None, fault_obj=None): super(ClientWidget, self).__init__(parent) SaveHistoryMixin.__init__(self, history_filename) # --- Init attrs self.container = parent self.id_ = id_ self.connection_file = connection_file self.hostname = hostname self.menu_actions = menu_actions self.is_external_kernel = is_external_kernel self.given_name = given_name self.show_elapsed_time = show_elapsed_time self.reset_warning = reset_warning self.ask_before_restart = ask_before_restart self.ask_before_closing = ask_before_closing # --- Other attrs self.context_menu_actions = context_menu_actions self.time_label = time_label self.options_button = options_button self.history = [] self.allow_rename = True self.is_error_shown = False self.error_text = None self.restart_thread = None self.give_focus = give_focus if css_path is None: self.css_path = CSS_PATH else: self.css_path = css_path # --- Widgets self.shellwidget = ShellWidget( config=config_options, ipyclient=self, additional_options=additional_options, interpreter_versions=interpreter_versions, is_external_kernel=is_external_kernel, is_spyder_kernel=is_spyder_kernel, handlers=handlers, local_kernel=True ) self.infowidget = self.container.infowidget self.blank_page = self._create_blank_page() self.loading_page = self._create_loading_page() # To keep a reference to the page to be displayed # in infowidget self.info_page = None self._before_prompt_is_ready() # Elapsed time self.t0 = time.monotonic() self.timer = QTimer(self) # --- Layout self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.shellwidget) if self.infowidget is not None: self.layout.addWidget(self.infowidget) self.setLayout(self.layout) # --- Exit function self.exit_callback = lambda: self.container.close_client(client=self) # --- Dialog manager self.dialog_manager = DialogManager() # --- Standard files handling self.stderr_obj = stderr_obj self.stdout_obj = stdout_obj self.fault_obj = fault_obj self.std_poll_timer = None if self.stderr_obj is not None or self.stdout_obj is not None: self.std_poll_timer = QTimer(self) self.std_poll_timer.timeout.connect(self.poll_std_file_change) self.std_poll_timer.setInterval(1000) self.std_poll_timer.start() self.shellwidget.executed.connect(self.poll_std_file_change) self.start_successful = False def __del__(self): """Close threads to avoid segfault.""" if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.quit() self.restart_thread.wait() # ----- Private methods --------------------------------------------------- def _before_prompt_is_ready(self, show_loading_page=True): """Configuration before kernel is connected.""" if show_loading_page: self._show_loading_page() self.shellwidget.sig_prompt_ready.connect( self._when_prompt_is_ready) # If remote execution, the loading page should be hidden as well self.shellwidget.sig_remote_execute.connect( self._when_prompt_is_ready) def _when_prompt_is_ready(self): """Configuration after the prompt is shown.""" self.start_successful = True # To hide the loading page self._hide_loading_page() # Show possible errors when setting Matplotlib backend self._show_mpl_backend_errors() # To show if special console is valid self._check_special_console_error() # Set the initial current working directory self._set_initial_cwd() self.shellwidget.sig_prompt_ready.disconnect( self._when_prompt_is_ready) self.shellwidget.sig_remote_execute.disconnect( self._when_prompt_is_ready) # It's necessary to do this at this point to avoid giving # focus to _control at startup. self._connect_control_signals() if self.give_focus: self.shellwidget._control.setFocus() def _create_loading_page(self): """Create html page to show while the kernel is starting""" loading_template = Template(LOADING) loading_img = get_image_path('loading_sprites') if os.name == 'nt': loading_img = loading_img.replace('\\', '/') message = _("Connecting to kernel...") page = loading_template.substitute(css_path=self.css_path, loading_img=loading_img, message=message) return page def _create_blank_page(self): """Create html page to show while the kernel is starting""" loading_template = Template(BLANK) page = loading_template.substitute(css_path=self.css_path) return page def _show_loading_page(self): """Show animation while the kernel is loading.""" if self.infowidget is not None: self.shellwidget.hide() self.infowidget.show() self.info_page = self.loading_page self.set_info_page() def _hide_loading_page(self): """Hide animation shown while the kernel is loading.""" if self.infowidget is not None: self.infowidget.hide() self.info_page = self.blank_page self.set_info_page() self.shellwidget.show() def _read_stderr(self): """Read the stderr file of the kernel.""" # We need to read stderr_file as bytes to be able to # detect its encoding with chardet f = open(self.stderr_file, 'rb') try: stderr_text = f.read() # This is needed to avoid showing an empty error message # when the kernel takes too much time to start. # See spyder-ide/spyder#8581. if not stderr_text: return '' # This is needed since the stderr file could be encoded # in something different to utf-8. # See spyder-ide/spyder#4191. encoding = get_coding(stderr_text) stderr_text = to_text_string(stderr_text, encoding) return stderr_text finally: f.close() def _show_mpl_backend_errors(self): """ Show possible errors when setting the selected Matplotlib backend. """ if self.shellwidget.is_spyder_kernel: self.shellwidget.call_kernel().show_mpl_backend_errors() def _check_special_console_error(self): """Check if the dependecies for special consoles are available.""" self.shellwidget.call_kernel( callback=self._show_special_console_error ).is_special_kernel_valid() def _show_special_console_error(self, missing_dependency): if missing_dependency is not None: error_message = _( "Your Python environment or installation doesn't have the " "<tt>{missing_dependency}</tt> module installed or it " "occurred a problem importing it. Due to that, it is not " "possible for Spyder to create this special console for " "you." ).format(missing_dependency=missing_dependency) self.show_kernel_error(error_message) def _abort_kernel_restart(self): """ Abort kernel restart if there are errors while starting it. We also ignore errors about comms, which are irrelevant. """ if self.start_successful: return False stderr = self.stderr_obj.get_contents() if not stderr: return False # There is an error. If it is benign, ignore. for line in stderr.splitlines(): if line and not self.is_benign_error(line): return True return False def _connect_control_signals(self): """Connect signals of control widgets.""" control = self.shellwidget._control page_control = self.shellwidget._page_control control.sig_focus_changed.connect( self.container.sig_focus_changed) page_control.sig_focus_changed.connect( self.container.sig_focus_changed) control.sig_visibility_changed.connect( self.container.refresh_container) page_control.sig_visibility_changed.connect( self.container.refresh_container) page_control.sig_show_find_widget_requested.connect( self.container.find_widget.show) def _set_initial_cwd(self): """Set initial cwd according to preferences.""" logger.debug("Setting initial working directory") cwd_path = get_home_dir() project_path = self.container.get_active_project_path() # This is for the first client if self.id_['int_id'] == '1': if self.get_conf( 'startup/use_project_or_home_directory', section='workingdir' ): cwd_path = get_home_dir() if project_path is not None: cwd_path = project_path elif self.get_conf( 'startup/use_fixed_directory', section='workingdir' ): cwd_path = self.get_conf( 'startup/fixed_directory', default=get_home_dir(), section='workingdir' ) else: # For new clients if self.get_conf( 'console/use_project_or_home_directory', section='workingdir' ): cwd_path = get_home_dir() if project_path is not None: cwd_path = project_path elif self.get_conf('console/use_cwd', section='workingdir'): cwd_path = self.container.get_working_directory() elif self.get_conf( 'console/use_fixed_directory', section='workingdir' ): cwd_path = self.get_conf( 'console/fixed_directory', default=get_home_dir(), section='workingdir' ) if osp.isdir(cwd_path): self.shellwidget.set_cwd(cwd_path) # ----- Public API -------------------------------------------------------- @property def kernel_id(self): """Get kernel id.""" if self.connection_file is not None: json_file = osp.basename(self.connection_file) return json_file.split('.json')[0] def remove_std_files(self, is_last_client=True): """Remove stderr_file associated with the client.""" try: self.shellwidget.executed.disconnect(self.poll_std_file_change) except TypeError: pass if self.std_poll_timer is not None: self.std_poll_timer.stop() if is_last_client: if self.stderr_obj is not None: self.stderr_obj.remove() if self.stdout_obj is not None: self.stdout_obj.remove() if self.fault_obj is not None: self.fault_obj.remove() @Slot() def poll_std_file_change(self): """Check if the stderr or stdout file just changed.""" self.shellwidget.call_kernel().flush_std() starting = self.shellwidget._starting if self.stderr_obj is not None: stderr = self.stderr_obj.poll_file_change() if stderr: if self.is_benign_error(stderr): return if self.shellwidget.isHidden(): # Avoid printing the same thing again if self.error_text != '<tt>%s</tt>' % stderr: full_stderr = self.stderr_obj.get_contents() self.show_kernel_error('<tt>%s</tt>' % full_stderr) if starting: self.shellwidget.banner = ( stderr + '\n' + self.shellwidget.banner) else: self.shellwidget._append_plain_text( '\n' + stderr, before_prompt=True) if self.stdout_obj is not None: stdout = self.stdout_obj.poll_file_change() if stdout: if starting: self.shellwidget.banner = ( stdout + '\n' + self.shellwidget.banner) else: self.shellwidget._append_plain_text( '\n' + stdout, before_prompt=True) def configure_shellwidget(self, give_focus=True): """Configure shellwidget after kernel is connected.""" self.give_focus = give_focus # Make sure the kernel sends the comm config over self.shellwidget.call_kernel()._send_comm_config() # Set exit callback self.shellwidget.set_exit_callback() # To save history self.shellwidget.executing.connect(self.add_to_history) # For Mayavi to run correctly self.shellwidget.executing.connect( self.shellwidget.set_backend_for_mayavi) # To update history after execution self.shellwidget.executed.connect(self.update_history) # To update the Variable Explorer after execution self.shellwidget.executed.connect( self.shellwidget.refresh_namespacebrowser) # To enable the stop button when executing a process self.shellwidget.executing.connect( self.sig_execution_state_changed) # To disable the stop button after execution stopped self.shellwidget.executed.connect( self.sig_execution_state_changed) # To show kernel restarted/died messages self.shellwidget.sig_kernel_restarted_message.connect( self.kernel_restarted_message) self.shellwidget.sig_kernel_restarted.connect( self._finalise_restart) # To correctly change Matplotlib backend interactively self.shellwidget.executing.connect( self.shellwidget.change_mpl_backend) # To show env and sys.path contents self.shellwidget.sig_show_syspath.connect(self.show_syspath) self.shellwidget.sig_show_env.connect(self.show_env) # To sync with working directory toolbar self.shellwidget.executed.connect(self.shellwidget.update_cwd) # To apply style self.set_color_scheme(self.shellwidget.syntax_style, reset=False) if self.fault_obj is not None: # To display faulthandler self.shellwidget.call_kernel().enable_faulthandler( self.fault_obj.filename) def add_to_history(self, command): """Add command to history""" if self.shellwidget.is_debugging(): return return super(ClientWidget, self).add_to_history(command) def is_client_executing(self): return (self.shellwidget._executing or self.shellwidget.is_waiting_pdb_input()) @Slot() def stop_button_click_handler(self): """Method to handle what to do when the stop button is pressed""" # Interrupt computations or stop debugging if not self.shellwidget.is_waiting_pdb_input(): self.interrupt_kernel() else: self.shellwidget.pdb_execute_command('exit') def show_kernel_error(self, error): """Show kernel initialization errors in infowidget.""" self.error_text = error if self.is_benign_error(error): return InstallerIPythonKernelError(error) # Replace end of line chars with <br> eol = sourcecode.get_eol_chars(error) if eol: error = error.replace(eol, '<br>') # Don't break lines in hyphens # From https://stackoverflow.com/q/7691569/438386 error = error.replace('-', '‑') # Create error page message = _("An error ocurred while starting the kernel") kernel_error_template = Template(KERNEL_ERROR) self.info_page = kernel_error_template.substitute( css_path=self.css_path, message=message, error=error) # Show error if self.infowidget is not None: self.set_info_page() self.shellwidget.hide() self.infowidget.show() # Tell the client we're in error mode self.is_error_shown = True # Stop shellwidget self.shellwidget.shutdown() self.remove_std_files(is_last_client=False) def is_benign_error(self, error): """Decide if an error is benign in order to filter it.""" benign_errors = [ # Error when switching from the Qt5 backend to the Tk one. # See spyder-ide/spyder#17488 "KeyboardInterrupt caught in kernel", "QSocketNotifier: Multiple socket notifiers for same socket", # Error when switching from the Tk backend to the Qt5 one. # See spyder-ide/spyder#17488 "Tcl_AsyncDelete async handler deleted by the wrong thread", "error in background error handler:", " while executing", '"::tcl::Bgerror', # Avoid showing this warning because it was up to the user to # disable secure writes. "WARNING: Insecure writes have been enabled via environment", # Old error "No such comm" ] return any([err in error for err in benign_errors]) def get_name(self): """Return client name""" if self.given_name is None: # Name according to host if self.hostname is None: name = _("Console") else: name = self.hostname # Adding id to name client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] name = name + u' ' + client_id elif self.given_name in ["Pylab", "SymPy", "Cython"]: client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] name = self.given_name + u' ' + client_id else: name = self.given_name + u'/' + self.id_['str_id'] return name def get_control(self): """Return the text widget (or similar) to give focus to""" # page_control is the widget used for paging page_control = self.shellwidget._page_control if page_control and page_control.isVisible(): return page_control else: return self.shellwidget._control def get_kernel(self): """Get kernel associated with this client""" return self.shellwidget.kernel_manager def add_actions_to_context_menu(self, menu): """Add actions to IPython widget context menu""" add_actions(menu, self.context_menu_actions) return menu def set_font(self, font): """Set IPython widget's font""" self.shellwidget._control.setFont(font) self.shellwidget.font = font def set_color_scheme(self, color_scheme, reset=True): """Set IPython color scheme.""" # Needed to handle not initialized kernel_client # See spyder-ide/spyder#6996. try: self.shellwidget.set_color_scheme(color_scheme, reset) except AttributeError: pass def shutdown(self, is_last_client): """Shutdown connection and kernel if needed.""" self.dialog_manager.close_all() if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.finished.disconnect() self.restart_thread.quit() self.restart_thread.wait() shutdown_kernel = ( is_last_client and not self.is_external_kernel and not self.is_error_shown) self.shellwidget.shutdown(shutdown_kernel) self.remove_std_files(shutdown_kernel) def interrupt_kernel(self): """Interrupt the associanted Spyder kernel if it's running""" # Needed to prevent a crash when a kernel is not running. # See spyder-ide/spyder#6299. try: self.shellwidget.request_interrupt_kernel() except RuntimeError: pass @Slot() def restart_kernel(self): """ Restart the associated kernel. Took this code from the qtconsole project Licensed under the BSD license """ sw = self.shellwidget if not running_under_pytest() and self.ask_before_restart: message = _('Are you sure you want to restart the kernel?') buttons = QMessageBox.Yes | QMessageBox.No result = QMessageBox.question(self, _('Restart kernel?'), message, buttons) else: result = None if (result == QMessageBox.Yes or running_under_pytest() or not self.ask_before_restart): if sw.kernel_manager: if self.infowidget is not None: if self.infowidget.isVisible(): self.infowidget.hide() if self._abort_kernel_restart(): sw.spyder_kernel_comm.close() return self._show_loading_page() # Close comm sw.spyder_kernel_comm.close() # Stop autorestart mechanism sw.kernel_manager.stop_restarter() sw.kernel_manager.autorestart = False # Reconfigure client before the new kernel is connected again. self._before_prompt_is_ready(show_loading_page=False) # Create and run restarting thread if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.finished.disconnect() self.restart_thread.quit() self.restart_thread.wait() self.restart_thread = QThread(None) self.restart_thread.run = self._restart_thread_main self.restart_thread.error = None self.restart_thread.finished.connect( lambda: self._finalise_restart(True)) self.restart_thread.start() else: sw._append_plain_text( _('Cannot restart a kernel not started by Spyder\n'), before_prompt=True ) self._hide_loading_page() def _restart_thread_main(self): """Restart the kernel in a thread.""" try: self.shellwidget.kernel_manager.restart_kernel( stderr=self.stderr_obj.handle, stdout=self.stdout_obj.handle) except RuntimeError as e: self.restart_thread.error = e def _finalise_restart(self, reset=False): """Finishes the restarting of the kernel.""" sw = self.shellwidget if self._abort_kernel_restart(): sw.spyder_kernel_comm.close() return if self.restart_thread and self.restart_thread.error is not None: sw._append_plain_text( _('Error restarting kernel: %s\n') % self.restart_thread.error, before_prompt=True ) else: if self.fault_obj is not None: fault = self.fault_obj.get_contents() if fault: fault = self.filter_fault(fault) self.shellwidget._append_plain_text( '\n' + fault, before_prompt=True) # Reset Pdb state and reopen comm sw._pdb_in_loop = False sw.spyder_kernel_comm.remove() try: sw.spyder_kernel_comm.open_comm(sw.kernel_client) except AttributeError: # An error occurred while opening our comm channel. # Aborting! return # Start autorestart mechanism sw.kernel_manager.autorestart = True sw.kernel_manager.start_restarter() # For spyder-ide/spyder#6235, IPython was changing the # setting of %colors on windows by assuming it was using a # dark background. This corrects it based on the scheme. self.set_color_scheme(sw.syntax_style, reset=reset) sw._append_html(_("<br>Restarting kernel...<br>"), before_prompt=True) sw.insert_horizontal_ruler() if self.fault_obj is not None: self.shellwidget.call_kernel().enable_faulthandler( self.fault_obj.filename) self._hide_loading_page() self.restart_thread = None self.sig_execution_state_changed.emit() def filter_fault(self, fault): """Get a fault from a previous session.""" thread_regex = ( r"(Current thread|Thread) " r"(0x[\da-f]+) \(most recent call first\):" r"(?:.|\r\n|\r|\n)+?(?=Current thread|Thread|\Z)") # Keep line for future improvments # files_regex = r"File \"([^\"]+)\", line (\d+) in (\S+)" main_re = "Main thread id:(?:\r\n|\r|\n)(0x[0-9a-f]+)" main_id = 0 for match in re.finditer(main_re, fault): main_id = int(match.group(1), base=16) system_re = ("System threads ids:" "(?:\r\n|\r|\n)(0x[0-9a-f]+(?: 0x[0-9a-f]+)+)") ignore_ids = [] start_idx = 0 for match in re.finditer(system_re, fault): ignore_ids = [int(i, base=16) for i in match.group(1).split()] start_idx = match.span()[1] text = "" for idx, match in enumerate(re.finditer(thread_regex, fault)): if idx == 0: text += fault[start_idx:match.span()[0]] thread_id = int(match.group(2), base=16) if thread_id != main_id: if thread_id in ignore_ids: continue if "wurlitzer.py" in match.group(0): # Wurlitzer threads are launched later continue text += "\n" + match.group(0) + "\n" else: try: pattern = (r".*(?:/IPython/core/interactiveshell\.py|" r"\\IPython\\core\\interactiveshell\.py).*") match_internal = next(re.finditer(pattern, match.group(0))) end_idx = match_internal.span()[0] except StopIteration: end_idx = None text += "\nMain thread:\n" + match.group(0)[:end_idx] + "\n" return text @Slot(str) def kernel_restarted_message(self, msg): """Show kernel restarted/died messages.""" if self.stderr_obj is not None: # If there are kernel creation errors, jupyter_client will # try to restart the kernel and qtconsole prints a # message about it. # So we read the kernel's stderr_file and display its # contents in the client instead of the usual message shown # by qtconsole. self.poll_std_file_change() else: self.shellwidget._append_html("<br>%s<hr><br>" % msg, before_prompt=False) @Slot() def enter_array_inline(self): """Enter and show the array builder on inline mode.""" self.shellwidget._control.enter_array_inline() @Slot() def enter_array_table(self): """Enter and show the array builder on table.""" self.shellwidget._control.enter_array_table() @Slot() def inspect_object(self): """Show how to inspect an object with our Help plugin""" self.shellwidget._control.inspect_current_object() @Slot() def clear_line(self): """Clear a console line""" self.shellwidget._keyboard_quit() @Slot() def clear_console(self): """Clear the whole console""" self.shellwidget.clear_console() @Slot() def reset_namespace(self): """Resets the namespace by removing all names defined by the user""" self.shellwidget.reset_namespace(warning=self.reset_warning, message=True) def update_history(self): self.history = self.shellwidget._history @Slot(object) def show_syspath(self, syspath): """Show sys.path contents.""" if syspath is not None: editor = CollectionsEditor(self) editor.setup(syspath, title="sys.path contents", readonly=True, icon=ima.icon('syspath')) self.dialog_manager.show(editor) else: return @Slot(object) def show_env(self, env): """Show environment variables.""" self.dialog_manager.show(RemoteEnvDialog(env, parent=self)) def show_time(self, end=False): """Text to show in time_label.""" if self.time_label is None: return elapsed_time = time.monotonic() - self.t0 # System time changed to past date, so reset start. if elapsed_time < 0: self.t0 = time.monotonic() elapsed_time = 0 if elapsed_time > 24 * 3600: # More than a day...! fmt = "%d %H:%M:%S" else: fmt = "%H:%M:%S" if end: color = QStylePalette.COLOR_TEXT_3 else: color = QStylePalette.COLOR_ACCENT_4 text = "<span style=\'color: %s\'><b>%s" \ "</b></span>" % (color, time.strftime(fmt, time.gmtime(elapsed_time))) if self.show_elapsed_time: self.time_label.setText(text) else: self.time_label.setText("") @Slot(bool) def set_show_elapsed_time(self, state): """Slot to show/hide elapsed time label.""" self.show_elapsed_time = state def set_info_page(self): """Set current info_page.""" if self.infowidget is not None and self.info_page is not None: self.infowidget.setHtml( self.info_page, QUrl.fromLocalFile(self.css_path) )