Ejemplo n.º 1
0
class Console(SpyderPluginWidget):
    """
    Console widget
    """
    CONF_SECTION = 'internal_console'
    focus_changed = Signal()
    redirect_stdio = Signal(bool)
    edit_goto = Signal(str, int, str)

    def __init__(self,
                 parent=None,
                 namespace=None,
                 commands=[],
                 message=None,
                 exitfunc=None,
                 profile=False,
                 multithreaded=False):
        SpyderPluginWidget.__init__(self, parent)
        logger.info("Initializing...")
        self.dialog_manager = DialogManager()

        # Shell
        self.shell = InternalShell(parent, namespace, commands, message,
                                   self.get_option('max_line_count'),
                                   self.get_font(), exitfunc, profile,
                                   multithreaded)
        self.shell.status.connect(
            lambda msg: self.sig_show_status_message.emit(msg, 0))
        self.shell.go_to_error.connect(self.go_to_error)
        self.shell.focus_changed.connect(lambda: self.focus_changed.emit())

        # Redirecting some signals:
        self.shell.redirect_stdio.connect(
            lambda state: self.redirect_stdio.emit(state))

        # Find/replace widget
        self.find_widget = FindReplace(self)
        self.find_widget.set_editor(self.shell)
        self.find_widget.hide()
        self.register_widget_shortcuts(self.find_widget)

        # Main layout
        btn_layout = QHBoxLayout()
        btn_layout.setAlignment(Qt.AlignLeft)
        btn_layout.addStretch()
        btn_layout.addWidget(self.options_button, Qt.AlignRight)
        layout = create_plugin_layout(btn_layout)
        layout.addWidget(self.shell)
        layout.addWidget(self.find_widget)
        self.setLayout(layout)

        # Parameters
        self.shell.toggle_wrap_mode(self.get_option('wrap'))

        # Accepting drops
        self.setAcceptDrops(True)

        # Traceback MessageBox
        self.error_dlg = None
        self.error_traceback = ""
        self.dismiss_error = False

    #------ Private API --------------------------------------------------------
    def set_historylog(self, historylog):
        """Bind historylog instance to this console
        Not used anymore since v2.0"""
        historylog.add_history(self.shell.history_filename)
        self.shell.append_to_history.connect(historylog.append_to_history)

    def set_help(self, help_plugin):
        """Bind help instance to this console"""
        self.shell.help = help_plugin

    #------ SpyderPluginWidget API ---------------------------------------------
    def get_plugin_title(self):
        """Return widget title"""
        return _('Internal console')

    def get_focus_widget(self):
        """
        Return the widget to give focus to when
        this plugin's dockwidget is raised on top-level
        """
        return self.shell

    def update_font(self):
        """Update font from Preferences"""
        font = self.get_font()
        self.shell.set_font(font)

    def closing_plugin(self, cancelable=False):
        """Perform actions before parent main window is closed"""
        self.dialog_manager.close_all()
        self.shell.exit_interpreter()
        return True

    def get_plugin_actions(self):
        """Return a list of actions related to plugin"""
        quit_action = create_action(self,
                                    _("&Quit"),
                                    icon=ima.icon('exit'),
                                    tip=_("Quit"),
                                    triggered=self.quit)
        self.register_shortcut(quit_action, "_", "Quit", "Ctrl+Q")
        run_action = create_action(self,
                                   _("&Run..."),
                                   None,
                                   ima.icon('run_small'),
                                   _("Run a Python script"),
                                   triggered=self.run_script)
        environ_action = create_action(
            self,
            _("Environment variables..."),
            icon=ima.icon('environ'),
            tip=_("Show and edit environment variables"
                  " (for current session)"),
            triggered=self.show_env)
        syspath_action = create_action(self,
                                       _("Show sys.path contents..."),
                                       icon=ima.icon('syspath'),
                                       tip=_("Show (read-only) sys.path"),
                                       triggered=self.show_syspath)
        buffer_action = create_action(self,
                                      _("Buffer..."),
                                      None,
                                      tip=_("Set maximum line count"),
                                      triggered=self.change_max_line_count)
        exteditor_action = create_action(
            self,
            _("External editor path..."),
            None,
            None,
            _("Set external editor executable path"),
            triggered=self.change_exteditor)
        wrap_action = create_action(self,
                                    _("Wrap lines"),
                                    toggled=self.toggle_wrap_mode)
        wrap_action.setChecked(self.get_option('wrap'))
        codecompletion_action = create_action(
            self,
            _("Automatic code completion"),
            toggled=self.toggle_codecompletion)
        codecompletion_action.setChecked(
            self.get_option('codecompletion/auto'))

        option_menu = QMenu(_('Internal console settings'), self)
        option_menu.setIcon(ima.icon('tooloptions'))
        add_actions(option_menu, (buffer_action, wrap_action,
                                  codecompletion_action, exteditor_action))

        plugin_actions = [
            None, run_action, environ_action, syspath_action, option_menu,
            MENU_SEPARATOR, quit_action
        ]

        return plugin_actions

    def register_plugin(self):
        """Register plugin in Spyder's main window"""
        self.focus_changed.connect(self.main.plugin_focus_changed)
        self.add_dockwidget()
        # Connecting the following signal once the dockwidget has been created:
        self.shell.exception_occurred.connect(self.exception_occurred)

    def exception_occurred(self, text, is_traceback, is_pyls_error=False):
        """
        Exception ocurred in the internal console.

        Show a QDialog or the internal console to warn the user.
        """
        # Skip errors without traceback or dismiss
        if (not is_traceback and self.error_dlg is None) or self.dismiss_error:
            return

        if CONF.get('main', 'show_internal_errors'):
            if self.error_dlg is None:
                self.error_dlg = SpyderErrorDialog(self)
                self.error_dlg.set_color_scheme(
                    CONF.get('appearance', 'selected'))
                self.error_dlg.close_btn.clicked.connect(self.close_error_dlg)
                self.error_dlg.rejected.connect(self.remove_error_dlg)
                self.error_dlg.details.go_to_error.connect(self.go_to_error)

            if is_pyls_error:
                title = "Internal Python Language Server error"
                self.error_dlg.set_title(title)
                self.error_dlg.title.setEnabled(False)
            self.error_dlg.append_traceback(text)
            self.error_dlg.show()
        elif DEV or get_debug_level():
            self.switch_to_plugin()

    def close_error_dlg(self):
        """Close error dialog."""
        if self.error_dlg.dismiss_box.isChecked():
            self.dismiss_error = True
        self.error_dlg.reject()

    def remove_error_dlg(self):
        """Remove error dialog."""
        self.error_dlg = None

    #------ Public API ---------------------------------------------------------
    @Slot()
    def quit(self):
        """Quit mainwindow"""
        self.main.close()

    @Slot()
    def show_env(self):
        """Show environment variables"""
        self.dialog_manager.show(EnvDialog(parent=self))

    @Slot()
    def show_syspath(self):
        """Show sys.path"""
        editor = CollectionsEditor(parent=self)
        editor.setup(sys.path,
                     title="sys.path",
                     readonly=True,
                     width=600,
                     icon=ima.icon('syspath'))
        self.dialog_manager.show(editor)

    @Slot()
    def run_script(self,
                   filename=None,
                   silent=False,
                   set_focus=False,
                   args=None):
        """Run a Python script"""
        if filename is None:
            self.shell.interpreter.restore_stds()
            filename, _selfilter = getopenfilename(
                self, _("Run Python script"), getcwd_or_home(),
                _("Python scripts") + " (*.py ; *.pyw ; *.ipy)")
            self.shell.interpreter.redirect_stds()
            if filename:
                os.chdir(osp.dirname(filename))
                filename = osp.basename(filename)
            else:
                return
        logger.debug("Running script with %s", args)
        filename = osp.abspath(filename)
        rbs = remove_backslashes
        command = "runfile('%s', args='%s')" % (rbs(filename), rbs(args))
        if set_focus:
            self.shell.setFocus()
        if self.dockwidget:
            self.switch_to_plugin()
        self.shell.write(command + '\n')
        self.shell.run_command(command)

    def go_to_error(self, text):
        """Go to error if relevant"""
        match = get_error_match(to_text_string(text))
        if match:
            fname, lnb = match.groups()
            self.edit_script(fname, int(lnb))

    def edit_script(self, filename=None, goto=-1):
        """Edit script"""
        # Called from InternalShell
        if not hasattr(self, 'main') \
           or not hasattr(self.main, 'editor'):
            self.shell.external_editor(filename, goto)
            return
        if filename is not None:
            self.edit_goto.emit(osp.abspath(filename), goto, '')

    def execute_lines(self, lines):
        """Execute lines and give focus to shell"""
        self.shell.execute_lines(to_text_string(lines))
        self.shell.setFocus()

    @Slot()
    def change_max_line_count(self):
        "Change maximum line count" ""
        mlc, valid = QInputDialog.getInt(self, _('Buffer'),
                                         _('Maximum line count'),
                                         self.get_option('max_line_count'), 0,
                                         1000000)
        if valid:
            self.shell.setMaximumBlockCount(mlc)
            self.set_option('max_line_count', mlc)

    @Slot()
    def change_exteditor(self):
        """Change external editor path"""
        path, valid = QInputDialog.getText(
            self, _('External editor'), _('External editor executable path:'),
            QLineEdit.Normal, self.get_option('external_editor/path'))
        if valid:
            self.set_option('external_editor/path', to_text_string(path))

    @Slot(bool)
    def toggle_wrap_mode(self, checked):
        """Toggle wrap mode"""
        self.shell.toggle_wrap_mode(checked)
        self.set_option('wrap', checked)

    @Slot(bool)
    def toggle_codecompletion(self, checked):
        """Toggle automatic code completion"""
        self.shell.set_codecompletion_auto(checked)
        self.set_option('codecompletion/auto', checked)

    #----Drag and drop
    def dragEnterEvent(self, event):
        """Reimplement Qt method
        Inform Qt about the types of data that the widget accepts"""
        source = event.mimeData()
        if source.hasUrls():
            if mimedata2url(source):
                event.acceptProposedAction()
            else:
                event.ignore()
        elif source.hasText():
            event.acceptProposedAction()

    def dropEvent(self, event):
        """Reimplement Qt method
        Unpack dropped data and handle it"""
        source = event.mimeData()
        if source.hasUrls():
            pathlist = mimedata2url(source)
            self.shell.drop_pathlist(pathlist)
        elif source.hasText():
            lines = to_text_string(source.text())
            self.shell.set_cursor_position('eof')
            self.shell.execute_lines(lines)
        event.acceptProposedAction()
Ejemplo n.º 2
0
class ConsoleWidget(PluginMainWidget):
    # --- Signals
    # This signal emits a parsed error traceback text so we can then
    # request opening the file that traceback comes from in the Editor.
    sig_edit_goto_requested = Signal(str, int, str)

    # TODO: I do not think we use this?
    sig_focus_changed = Signal()

    # Emit this when the interpreter buffer is flushed
    sig_refreshed = Signal()

    # Request to show a status message on the main window
    sig_show_status_requested = Signal(str)

    # Request the main application to quit.
    sig_quit_requested = Signal()

    sig_help_requested = Signal(dict)
    """
    This signal is emitted to request help on a given object `name`.

    Parameters
    ----------
    help_data: dict
        Example `{'name': str, 'ignore_unknown': bool}`.
    """
    def __init__(self, name, plugin, parent=None):
        super().__init__(name, plugin, parent)

        logger.info("Initializing...")

        # Traceback MessageBox
        self.error_traceback = ''
        self.dismiss_error = False

        # Widgets
        self.dialog_manager = DialogManager()
        self.error_dlg = None
        self.shell = InternalShell(  # TODO: Move to use SpyderWidgetMixin?
            parent=parent,
            namespace=self.get_conf('namespace', {}),
            commands=self.get_conf('commands', []),
            message=self.get_conf('message', ''),
            max_line_count=self.get_conf('max_line_count'),
            profile=self.get_conf('profile', False),
            multithreaded=self.get_conf('multithreaded', False),
        )
        self.find_widget = FindReplace(self)

        # Setup
        self.setAcceptDrops(True)
        self.find_widget.set_editor(self.shell)
        self.find_widget.hide()
        self.shell.toggle_wrap_mode(self.get_conf('wrap'))

        # Layout
        layout = QVBoxLayout()
        layout.addWidget(self.shell)
        layout.addWidget(self.find_widget)
        self.setLayout(layout)

        # Signals
        self.shell.sig_help_requested.connect(self.sig_help_requested)
        self.shell.sig_exception_occurred.connect(self.handle_exception)
        self.shell.sig_focus_changed.connect(self.sig_focus_changed)
        self.shell.sig_go_to_error_requested.connect(self.go_to_error)
        self.shell.sig_redirect_stdio_requested.connect(
            self.sig_redirect_stdio_requested)
        self.shell.sig_refreshed.connect(self.sig_refreshed)
        self.shell.sig_show_status_requested.connect(
            lambda msg: self.sig_show_status_message.emit(msg, 0))

    # --- PluginMainWidget API
    # ------------------------------------------------------------------------
    def get_title(self):
        return _('Internal console')

    def setup(self):
        # TODO: Move this to the shell
        self.quit_action = self.create_action(
            ConsoleWidgetActions.Quit,
            text=_("&Quit"),
            tip=_("Quit"),
            icon=self.create_icon('exit'),
            triggered=self.sig_quit_requested,
            context=Qt.ApplicationShortcut,
            shortcut_context="_",
            register_shortcut=True)
        run_action = self.create_action(
            ConsoleWidgetActions.Run,
            text=_("&Run..."),
            tip=_("Run a Python script"),
            icon=self.create_icon('run_small'),
            triggered=self.run_script,
        )
        environ_action = self.create_action(
            ConsoleWidgetActions.Environment,
            text=_("Environment variables..."),
            tip=_("Show and edit environment variables (for current "
                  "session)"),
            icon=self.create_icon('environ'),
            triggered=self.show_env,
        )
        syspath_action = self.create_action(
            ConsoleWidgetActions.SysPath,
            text=_("Show sys.path contents..."),
            tip=_("Show (read-only) sys.path"),
            icon=self.create_icon('syspath'),
            triggered=self.show_syspath,
        )
        buffer_action = self.create_action(
            ConsoleWidgetActions.MaxLineCount,
            text=_("Buffer..."),
            tip=_("Set maximum line count"),
            triggered=self.change_max_line_count,
        )
        exteditor_action = self.create_action(
            ConsoleWidgetActions.ExternalEditor,
            text=_("External editor path..."),
            tip=_("Set external editor executable path"),
            triggered=self.change_exteditor,
        )
        wrap_action = self.create_action(
            ConsoleWidgetActions.ToggleWrap,
            text=_("Wrap lines"),
            toggled=lambda val: self.set_conf('wrap', val),
            initial=self.get_conf('wrap'),
        )
        codecompletion_action = self.create_action(
            ConsoleWidgetActions.ToggleCodeCompletion,
            text=_("Automatic code completion"),
            toggled=lambda val: self.set_conf('codecompletion/auto', val),
            initial=self.get_conf('codecompletion/auto'),
        )

        # Submenu
        internal_settings_menu = self.create_menu(
            ConsoleWidgetMenus.InternalSettings,
            _('Internal console settings'),
            icon=self.create_icon('tooloptions'),
        )
        for item in [
                buffer_action, wrap_action, codecompletion_action,
                exteditor_action
        ]:
            self.add_item_to_menu(
                item,
                menu=internal_settings_menu,
                section=ConsoleWidgetInternalSettingsSubMenuSections.Main,
            )

        # Options menu
        options_menu = self.get_options_menu()
        for item in [
                run_action, environ_action, syspath_action,
                internal_settings_menu
        ]:
            self.add_item_to_menu(
                item,
                menu=options_menu,
                section=ConsoleWidgetOptionsMenuSections.Run,
            )

        self.add_item_to_menu(
            self.quit_action,
            menu=options_menu,
            section=ConsoleWidgetOptionsMenuSections.Quit,
        )

        self.shell.set_external_editor(self.get_conf('external_editor/path'),
                                       '')

    @on_conf_change(option='max_line_count')
    def max_line_count_update(self, value):
        self.shell.setMaximumBlockCount(value)

    @on_conf_change(option='wrap')
    def wrap_mode_update(self, value):
        self.shell.toggle_wrap_mode(value)

    @on_conf_change(option='external_editor/path')
    def external_editor_update(self, value):
        self.shell.set_external_editor(value, '')

    def update_actions(self):
        pass

    def get_focus_widget(self):
        return self.shell

    # --- Qt overrides
    # ------------------------------------------------------------------------
    def dragEnterEvent(self, event):
        """
        Reimplement Qt method.

        Inform Qt about the types of data that the widget accepts.
        """
        source = event.mimeData()
        if source.hasUrls():
            if mimedata2url(source):
                event.acceptProposedAction()
            else:
                event.ignore()
        elif source.hasText():
            event.acceptProposedAction()

    def dropEvent(self, event):
        """
        Reimplement Qt method.

        Unpack dropped data and handle it.
        """
        source = event.mimeData()
        if source.hasUrls():
            pathlist = mimedata2url(source)
            self.shell.drop_pathlist(pathlist)
        elif source.hasText():
            lines = to_text_string(source.text())
            self.shell.set_cursor_position('eof')
            self.shell.execute_lines(lines)

        event.acceptProposedAction()

    # --- Public API
    # ------------------------------------------------------------------------
    def start_interpreter(self, namespace):
        """
        Start internal console interpreter.
        """
        self.shell.start_interpreter(namespace)

    def set_historylog(self, historylog):
        """
        Bind historylog instance to this console.

        Not used anymore since v2.0.
        """
        historylog.add_history(self.shell.history_filename)
        self.shell.sig_append_to_history_requested.connect(
            historylog.append_to_history)

    def set_help(self, help_plugin):
        """
        Bind help instance to this console.
        """
        self.shell.help = help_plugin

    def report_issue(self):
        """Report an issue with the SpyderErrorDialog."""
        self._report_dlg = SpyderErrorDialog(self, is_report=True)
        self._report_dlg.set_color_scheme(
            self.get_conf('selected', section='appearance'))
        self._report_dlg.show()

    @Slot(dict)
    def handle_exception(self, error_data, sender=None, internal_plugins=None):
        """
        Exception ocurred in the internal console.

        Show a QDialog or the internal console to warn the user.

        Handle any exception that occurs during Spyder usage.

        Parameters
        ----------
        error_data: dict
            The dictionary containing error data. The expected keys are:
            >>> error_data= {
                "text": str,
                "is_traceback": bool,
                "repo": str,
                "title": str,
                "label": str,
                "steps": str,
            }
        sender: spyder.api.plugins.SpyderPluginV2, optional
            The sender plugin. Default is None.

        Notes
        -----
        The `is_traceback` key indicates if `text` contains plain text or a
        Python error traceback.

        The `title` and `repo` keys indicate how the error data should
        customize the report dialog and Github error submission.

        The `label` and `steps` keys allow customizing the content of the
        error dialog.
        """
        text = error_data.get("text", None)
        is_traceback = error_data.get("is_traceback", False)
        title = error_data.get("title", "")
        label = error_data.get("label", "")
        steps = error_data.get("steps", "")

        # Skip errors without traceback (and no text) or dismiss
        if ((not text and not is_traceback and self.error_dlg is None)
                or self.dismiss_error):
            return

        # Retrieve internal plugins
        internal_plugins = PLUGIN_REGISTRY.internal_plugins

        # Get if sender is internal or not
        is_internal_plugin = True
        if sender is not None:
            sender_name = getattr(sender, 'NAME',
                                  getattr(sender, 'CONF_SECTION'))
            is_internal_plugin = sender_name in internal_plugins

        # Set repo
        repo = "spyder-ide/spyder"
        if not is_internal_plugin:
            repo = error_data.get("repo", None)

            if repo is None:
                raise SpyderAPIError(
                    f"External plugin '{sender_name}' does not define 'repo' "
                    "key in the 'error_data' dictionary in the form "
                    "my-org/my-repo (only Github is supported).")

            if repo == 'spyder-ide/spyder':
                raise SpyderAPIError(
                    f"External plugin '{sender_name}' 'repo' key needs to be "
                    "different from the main Spyder repo.")

        if self.get_conf('show_internal_errors', section='main'):
            if self.error_dlg is None:
                self.error_dlg = SpyderErrorDialog(self)
                self.error_dlg.set_color_scheme(
                    self.get_conf('selected', section='appearance'))
                self.error_dlg.close_btn.clicked.connect(self.close_error_dlg)
                self.error_dlg.rejected.connect(self.remove_error_dlg)
                self.error_dlg.details.sig_go_to_error_requested.connect(
                    self.go_to_error)

            # Set the report repository
            self.error_dlg.set_github_repo_org(repo)

            if title:
                self.error_dlg.set_title(title)
                self.error_dlg.title.setEnabled(False)

            if label:
                self.error_dlg.main_label.setText(label)
                self.error_dlg.submit_btn.setEnabled(True)

            if steps:
                self.error_dlg.steps_text.setText(steps)
                self.error_dlg.set_require_minimum_length(False)

            self.error_dlg.append_traceback(text)
            self.error_dlg.show()
        elif DEV or get_debug_level():
            self.change_visibility(True, True)

    def close_error_dlg(self):
        """
        Close error dialog.
        """
        if self.error_dlg.dismiss_box.isChecked():
            self.dismiss_error = True

        self.error_dlg.reject()

    def remove_error_dlg(self):
        """
        Remove error dialog.
        """
        self.error_dlg = None

    @Slot()
    def show_env(self):
        """
        Show environment variables.
        """
        self.dialog_manager.show(EnvDialog(parent=self))

    def get_sys_path(self):
        """
        Return the `sys.path`.
        """
        return sys.path

    @Slot()
    def show_syspath(self):
        """
        Show `sys.path`.
        """
        editor = CollectionsEditor(parent=self)
        editor.setup(
            sys.path,
            title="sys.path",
            readonly=True,
            icon=self.create_icon('syspath'),
        )
        self.dialog_manager.show(editor)

    @Slot()
    def run_script(self,
                   filename=None,
                   silent=False,
                   set_focus=False,
                   args=None):
        """
        Run a Python script.
        """
        if filename is None:
            self.shell.interpreter.restore_stds()
            filename, _selfilter = getopenfilename(
                self,
                _("Run Python script"),
                getcwd_or_home(),
                _("Python scripts") + " (*.py ; *.pyw ; *.ipy)",
            )
            self.shell.interpreter.redirect_stds()

            if filename:
                os.chdir(osp.dirname(filename))
                filename = osp.basename(filename)
            else:
                return

        logger.debug("Running script with %s", args)
        filename = osp.abspath(filename)
        rbs = remove_backslashes
        command = "runfile('%s', args='%s')" % (rbs(filename), rbs(args))

        if set_focus:
            self.shell.setFocus()

        self.change_visibility(True, True)

        self.shell.write(command + '\n')
        self.shell.run_command(command)

    def go_to_error(self, text):
        """
        Go to error if relevant.
        """
        match = get_error_match(to_text_string(text))
        if match:
            fname, lnb = match.groups()
            self.edit_script(fname, int(lnb))

    def edit_script(self, filename=None, goto=-1):
        """
        Edit script.
        """
        if filename is not None:
            # Called from InternalShell
            self.shell.external_editor(filename, goto)
            self.sig_edit_goto_requested.emit(osp.abspath(filename), goto, '')

    def execute_lines(self, lines):
        """
        Execute lines and give focus to shell.
        """
        self.shell.execute_lines(to_text_string(lines))
        self.shell.setFocus()

    @Slot()
    def change_max_line_count(self, value=None):
        """"
        Change maximum line count.
        """
        valid = True
        if value is None:
            value, valid = QInputDialog.getInt(
                self,
                _('Buffer'),
                _('Maximum line count'),
                self.get_conf('max_line_count'),
                0,
                1000000,
            )

        if valid:
            self.set_conf('max_line_count', value)

    @Slot()
    def change_exteditor(self, path=None):
        """
        Change external editor path.
        """
        valid = True
        if path is None:
            path, valid = QInputDialog.getText(
                self,
                _('External editor'),
                _('External editor executable path:'),
                QLineEdit.Normal,
                self.get_conf('external_editor/path'),
            )

        if valid:
            self.set_conf('external_editor/path', to_text_string(path))

    def set_exit_function(self, func):
        """
        Set the callback function to execute when the `exit_interpreter` is
        called.
        """
        self.shell.exitfunc = func

    def set_font(self, font):
        """
        Set font of the internal shell.
        """
        self.shell.set_font(font)

    def redirect_stds(self):
        """
        Redirect stdout and stderr when using open file dialogs.
        """
        self.shell.interpreter.redirect_stds()

    def restore_stds(self):
        """
        Restore stdout and stderr when using open file dialogs.
        """
        self.shell.interpreter.restore_stds()

    def set_namespace_item(self, name, item):
        """
        Add an object to the namespace dictionary of the internal console.
        """
        self.shell.interpreter.namespace[name] = item

    def exit_interpreter(self):
        """
        Exit the internal console interpreter.

        This is equivalent to requesting the main application to quit.
        """
        self.shell.exit_interpreter()
Ejemplo n.º 3
0
class Console(SpyderPluginWidget):
    """
    Console widget
    """
    CONF_SECTION = 'internal_console'
    focus_changed = Signal()
    redirect_stdio = Signal(bool)
    edit_goto = Signal(str, int, str)
    
    def __init__(self, parent=None, namespace=None, commands=[], message=None,
                 exitfunc=None, profile=False, multithreaded=False):
        SpyderPluginWidget.__init__(self, parent)

        logger.info("Initializing...")
        self.dialog_manager = DialogManager()

        # Shell
        light_background = self.get_option('light_background')
        self.shell = InternalShell(parent, namespace, commands, message,
                                   self.get_option('max_line_count'),
                                   self.get_plugin_font(), exitfunc, profile,
                                   multithreaded,
                                   light_background=light_background)
        self.shell.status.connect(lambda msg: self.show_message.emit(msg, 0))
        self.shell.go_to_error.connect(self.go_to_error)
        self.shell.focus_changed.connect(lambda: self.focus_changed.emit())

        # Redirecting some signals:
        self.shell.redirect_stdio.connect(lambda state:
                                          self.redirect_stdio.emit(state))
        
        # Initialize plugin
        self.initialize_plugin()

        # Find/replace widget
        self.find_widget = FindReplace(self)
        self.find_widget.set_editor(self.shell)
        self.find_widget.hide()
        self.register_widget_shortcuts(self.find_widget)

        # Main layout
        btn_layout = QHBoxLayout()
        btn_layout.setAlignment(Qt.AlignLeft)
        btn_layout.addStretch()
        btn_layout.addWidget(self.options_button, Qt.AlignRight)
        layout = create_plugin_layout(btn_layout)
        layout.addWidget(self.shell)
        layout.addWidget(self.find_widget)
        self.setLayout(layout)
        
        # Parameters
        self.shell.toggle_wrap_mode(self.get_option('wrap'))
            
        # Accepting drops
        self.setAcceptDrops(True)

        # Traceback MessageBox
        self.error_dlg = None
        self.error_traceback = ""
        self.dismiss_error = False

    #------ Private API --------------------------------------------------------
    def set_historylog(self, historylog):
        """Bind historylog instance to this console
        Not used anymore since v2.0"""
        historylog.add_history(self.shell.history_filename)
        self.shell.append_to_history.connect(historylog.append_to_history)

    def set_help(self, help_plugin):
        """Bind help instance to this console"""
        self.shell.help = help_plugin

    #------ SpyderPluginWidget API ---------------------------------------------
    def get_plugin_title(self):
        """Return widget title"""
        return _('Internal console')
    
    def get_focus_widget(self):
        """
        Return the widget to give focus to when
        this plugin's dockwidget is raised on top-level
        """
        return self.shell

    def update_font(self):
        """Update font from Preferences"""
        font = self.get_plugin_font()
        self.shell.set_font(font)

    def closing_plugin(self, cancelable=False):
        """Perform actions before parent main window is closed"""
        self.dialog_manager.close_all()
        self.shell.exit_interpreter()
        return True
        
    def refresh_plugin(self):
        pass
    
    def get_plugin_actions(self):
        """Return a list of actions related to plugin"""
        quit_action = create_action(self, _("&Quit"),
                                    icon=ima.icon('exit'), 
                                    tip=_("Quit"),
                                    triggered=self.quit)
        self.register_shortcut(quit_action, "_", "Quit", "Ctrl+Q")
        run_action = create_action(self, _("&Run..."), None,
                            ima.icon('run_small'),
                            _("Run a Python script"),
                            triggered=self.run_script)
        environ_action = create_action(self,
                            _("Environment variables..."),
                            icon=ima.icon('environ'),
                            tip=_("Show and edit environment variables"
                                        " (for current session)"),
                            triggered=self.show_env)
        syspath_action = create_action(self,
                            _("Show sys.path contents..."),
                            icon=ima.icon('syspath'),
                            tip=_("Show (read-only) sys.path"),
                            triggered=self.show_syspath)
        buffer_action = create_action(self,
                            _("Buffer..."), None,
                            tip=_("Set maximum line count"),
                            triggered=self.change_max_line_count)
        exteditor_action = create_action(self,
                            _("External editor path..."), None, None,
                            _("Set external editor executable path"),
                            triggered=self.change_exteditor)
        wrap_action = create_action(self,
                            _("Wrap lines"),
                            toggled=self.toggle_wrap_mode)
        wrap_action.setChecked(self.get_option('wrap'))
        calltips_action = create_action(self, _("Display balloon tips"),
            toggled=self.toggle_calltips)
        calltips_action.setChecked(self.get_option('calltips'))
        codecompletion_action = create_action(self,
                                          _("Automatic code completion"),
                                          toggled=self.toggle_codecompletion)
        codecompletion_action.setChecked(self.get_option('codecompletion/auto'))
        codecompenter_action = create_action(self,
                                    _("Enter key selects completion"),
                                    toggled=self.toggle_codecompletion_enter)
        codecompenter_action.setChecked(self.get_option(
                                                    'codecompletion/enter_key'))
        
        option_menu = QMenu(_('Internal console settings'), self)
        option_menu.setIcon(ima.icon('tooloptions'))
        add_actions(option_menu, (buffer_action, wrap_action,
                                  calltips_action, codecompletion_action,
                                  codecompenter_action, exteditor_action))
                    
        plugin_actions = [None, run_action, environ_action, syspath_action,
                          option_menu, MENU_SEPARATOR, quit_action,
                          self.undock_action]

        return plugin_actions
    
    def register_plugin(self):
        """Register plugin in Spyder's main window"""
        self.focus_changed.connect(self.main.plugin_focus_changed)
        self.main.add_dockwidget(self)
        # Connecting the following signal once the dockwidget has been created:
        self.shell.exception_occurred.connect(self.exception_occurred)
    
    def exception_occurred(self, text, is_traceback):
        """
        Exception ocurred in the internal console.

        Show a QDialog or the internal console to warn the user.
        """
        # Skip errors without traceback or dismiss
        if (not is_traceback and self.error_dlg is None) or self.dismiss_error:
            return

        if CONF.get('main', 'show_internal_errors'):
            if self.error_dlg is None:
                self.error_dlg = SpyderErrorDialog(self)
                self.error_dlg.close_btn.clicked.connect(self.close_error_dlg)
                self.error_dlg.rejected.connect(self.remove_error_dlg)
                self.error_dlg.details.go_to_error.connect(self.go_to_error)
                self.error_dlg.show()
            self.error_dlg.append_traceback(text)
        elif DEV or get_debug_level():
            self.dockwidget.show()
            self.dockwidget.raise_()

    def close_error_dlg(self):
        """Close error dialog."""
        if self.error_dlg.dismiss_box.isChecked():
            self.dismiss_error = True
        self.error_dlg.reject()

    def remove_error_dlg(self):
        """Remove error dialog."""
        self.error_dlg = None

    #------ Public API ---------------------------------------------------------
    @Slot()
    def quit(self):
        """Quit mainwindow"""
        self.main.close()
    
    @Slot()
    def show_env(self):
        """Show environment variables"""
        self.dialog_manager.show(EnvDialog())
    
    @Slot()
    def show_syspath(self):
        """Show sys.path"""
        editor = CollectionsEditor()
        editor.setup(sys.path, title="sys.path", readonly=True,
                     width=600, icon=ima.icon('syspath'))
        self.dialog_manager.show(editor)
    
    @Slot()
    def run_script(self, filename=None, silent=False, set_focus=False,
                   args=None):
        """Run a Python script"""
        if filename is None:
            self.shell.interpreter.restore_stds()
            filename, _selfilter = getopenfilename(
                    self, _("Run Python script"), getcwd_or_home(),
                    _("Python scripts")+" (*.py ; *.pyw ; *.ipy)")
            self.shell.interpreter.redirect_stds()
            if filename:
                os.chdir( osp.dirname(filename) )
                filename = osp.basename(filename)
            else:
                return
        logger.debug("Running script with %s", args)
        filename = osp.abspath(filename)
        rbs = remove_backslashes
        command = "runfile('%s', args='%s')" % (rbs(filename), rbs(args))
        if set_focus:
            self.shell.setFocus()
        if self.dockwidget and not self.ismaximized:
            self.dockwidget.setVisible(True)
            self.dockwidget.raise_()
        self.shell.write(command+'\n')
        self.shell.run_command(command)

            
    def go_to_error(self, text):
        """Go to error if relevant"""
        match = get_error_match(to_text_string(text))
        if match:
            fname, lnb = match.groups()
            self.edit_script(fname, int(lnb))
            
    def edit_script(self, filename=None, goto=-1):
        """Edit script"""
        # Called from InternalShell
        if not hasattr(self, 'main') \
           or not hasattr(self.main, 'editor'):
            self.shell.external_editor(filename, goto)
            return
        if filename is not None:
            self.edit_goto.emit(osp.abspath(filename), goto, '')
        
    def execute_lines(self, lines):
        """Execute lines and give focus to shell"""
        self.shell.execute_lines(to_text_string(lines))
        self.shell.setFocus()

    @Slot()
    def change_max_line_count(self):
        "Change maximum line count"""
        mlc, valid = QInputDialog.getInt(self, _('Buffer'),
                                           _('Maximum line count'),
                                           self.get_option('max_line_count'),
                                           0, 1000000)
        if valid:
            self.shell.setMaximumBlockCount(mlc)
            self.set_option('max_line_count', mlc)

    @Slot()
    def change_exteditor(self):
        """Change external editor path"""
        path, valid = QInputDialog.getText(self, _('External editor'),
                          _('External editor executable path:'),
                          QLineEdit.Normal,
                          self.get_option('external_editor/path'))
        if valid:
            self.set_option('external_editor/path', to_text_string(path))
    
    @Slot(bool)
    def toggle_wrap_mode(self, checked):
        """Toggle wrap mode"""
        self.shell.toggle_wrap_mode(checked)
        self.set_option('wrap', checked)
    
    @Slot(bool)
    def toggle_calltips(self, checked):
        """Toggle calltips"""
        self.shell.set_calltips(checked)
        self.set_option('calltips', checked)
    
    @Slot(bool)
    def toggle_codecompletion(self, checked):
        """Toggle automatic code completion"""
        self.shell.set_codecompletion_auto(checked)
        self.set_option('codecompletion/auto', checked)
    
    @Slot(bool)
    def toggle_codecompletion_enter(self, checked):
        """Toggle Enter key for code completion"""
        self.shell.set_codecompletion_enter(checked)
        self.set_option('codecompletion/enter_key', checked)
                
    #----Drag and drop                    
    def dragEnterEvent(self, event):
        """Reimplement Qt method
        Inform Qt about the types of data that the widget accepts"""
        source = event.mimeData()
        if source.hasUrls():
            if mimedata2url(source):
                event.acceptProposedAction()
            else:
                event.ignore()
        elif source.hasText():
            event.acceptProposedAction()
            
    def dropEvent(self, event):
        """Reimplement Qt method
        Unpack dropped data and handle it"""
        source = event.mimeData()
        if source.hasUrls():
            pathlist = mimedata2url(source)
            self.shell.drop_pathlist(pathlist)
        elif source.hasText():
            lines = to_text_string(source.text())
            self.shell.set_cursor_position('eof')
            self.shell.execute_lines(lines)
        event.acceptProposedAction()
Ejemplo n.º 4
0
class ConsoleWidget(PluginMainWidget):
    DEFAULT_OPTIONS = {
        'codecompletion/auto': True,
        'commands': [],
        'external_editor/gotoline': '',
        'external_editor/path': '',
        'max_line_count': 300,
        'message': 'Internal console\n\n',
        'multithreaded': False,
        'namespace': None,
        'profile': False,
        'show_internal_errors': True,
        'wrap': True,
        # From appearance
        'color_theme': 'spyder/dark',
    }

    # --- Signals
    # This signal emits a parsed error traceback text so we can then
    # request opening the file that traceback comes from in the Editor.
    sig_edit_goto_requested = Signal(str, int, str)

    # TODO: I do not think we use this?
    sig_focus_changed = Signal()

    # Emit this when the interpreter buffer is flushed
    sig_refreshed = Signal()

    # Request to show a status message on the main window
    sig_show_status_requested = Signal(str)

    # Request the main application to quit.
    sig_quit_requested = Signal()

    sig_help_requested = Signal(dict)
    """
    This signal is emitted to request help on a given object `name`.

    Parameters
    ----------
    help_data: dict
        Example `{'name': str, 'ignore_unknown': bool}`.
    """

    def __init__(self, name, plugin, parent=None, options=DEFAULT_OPTIONS):
        super().__init__(name, plugin, parent, options)

        logger.info("Initializing...")

        # Traceback MessageBox
        self.error_traceback = ''
        self.dismiss_error = False

        # Widgets
        self.dialog_manager = DialogManager()
        self.error_dlg = None
        self.shell = InternalShell(  # TODO: Move to use SpyderWidgetMixin?
            parent=parent,
            namespace=self.get_option('namespace'),
            commands=self.get_option('commands'),
            message=self.get_option('message'),
            max_line_count=self.get_option('max_line_count'),
            profile=self.get_option('profile'),
            multithreaded=self.get_option('multithreaded'),
        )
        self.find_widget = FindReplace(self)

        # Setup
        self.setAcceptDrops(True)
        self.find_widget.set_editor(self.shell)
        self.find_widget.hide()
        self.shell.toggle_wrap_mode(self.get_option('wrap'))

        # Layout
        layout = QVBoxLayout()
        layout.addWidget(self.shell)
        layout.addWidget(self.find_widget)
        self.setLayout(layout)

        # Signals
        self.shell.sig_help_requested.connect(self.sig_help_requested)
        self.shell.sig_exception_occurred.connect(self.handle_exception)
        self.shell.sig_focus_changed.connect(self.sig_focus_changed)
        self.shell.sig_go_to_error_requested.connect(self.go_to_error)
        self.shell.sig_redirect_stdio_requested.connect(
            self.sig_redirect_stdio_requested)
        self.shell.sig_refreshed.connect(self.sig_refreshed)
        self.shell.sig_show_status_requested.connect(
            lambda msg: self.sig_show_status_message.emit(msg, 0))

    # --- PluginMainWidget API
    # ------------------------------------------------------------------------
    def get_title(self):
        return _('Internal console')

    def setup(self, options):
        # TODO: Move this to the shell
        quit_action = self.create_action(
            ConsoleWidgetActions.Quit,
            text=_("&Quit"),
            tip=_("Quit"),
            icon=self.create_icon('exit'),
            triggered=self.sig_quit_requested,
            context=Qt.ApplicationShortcut,
        )
        run_action = self.create_action(
            ConsoleWidgetActions.Run,
            text=_("&Run..."),
            tip=_("Run a Python script"),
            icon=self.create_icon('run_small'),
            triggered=self.run_script,
        )
        environ_action = self.create_action(
            ConsoleWidgetActions.Environment,
            text=_("Environment variables..."),
            tip=_("Show and edit environment variables (for current "
                  "session)"),
            icon=self.create_icon('environ'),
            triggered=self.show_env,
        )
        syspath_action = self.create_action(
            ConsoleWidgetActions.SysPath,
            text=_("Show sys.path contents..."),
            tip=_("Show (read-only) sys.path"),
            icon=self.create_icon('syspath'),
            triggered=self.show_syspath,
        )
        buffer_action = self.create_action(
            ConsoleWidgetActions.MaxLineCount,
            text=_("Buffer..."),
            tip=_("Set maximum line count"),
            triggered=self.change_max_line_count,
        )
        exteditor_action = self.create_action(
            ConsoleWidgetActions.ExternalEditor,
            text=_("External editor path..."),
            tip=_("Set external editor executable path"),
            triggered=self.change_exteditor,
        )
        wrap_action = self.create_action(
            ConsoleWidgetActions.ToggleWrap,
            text=_("Wrap lines"),
            toggled=lambda val: self.set_option('wrap', val),
            initial=self.get_option('wrap'),
        )
        codecompletion_action = self.create_action(
            ConsoleWidgetActions.ToggleCodeCompletion,
            text=_("Automatic code completion"),
            toggled=lambda val: self.set_option('codecompletion/auto', val),
            initial=self.get_option('codecompletion/auto'),
        )

        # Submenu
        internal_settings_menu = self.create_menu(
            ConsoleWidgetMenus.InternalSettings,
            _('Internal console settings'),
        )
        for item in [buffer_action, wrap_action, codecompletion_action,
                     exteditor_action]:
            self.add_item_to_menu(
                item,
                menu=internal_settings_menu,
                section=ConsoleWidgetInternalSettingsSubMenuSections.Main,
            )

        # Options menu
        options_menu = self.get_options_menu()
        for item in [run_action, environ_action, syspath_action,
                     internal_settings_menu]:
            self.add_item_to_menu(
                item,
                menu=options_menu,
                section=ConsoleWidgetOptionsMenuSections.Run,
            )

        self.add_item_to_menu(
            quit_action,
            menu=options_menu,
            section=ConsoleWidgetOptionsMenuSections.Quit,
        )

        self.shell.set_external_editor(
            self.get_option('external_editor/path'), '')

    def on_option_update(self, option, value):
        if option == 'max_line_count':
            self.shell.setMaximumBlockCount(value)
        elif option == 'wrap':
            self.shell.toggle_wrap_mode(value)
        elif option == 'codecompletion/auto':
            self.shell.set_codecompletion_auto(value)
        elif option == 'external_editor/path':
            self.shell.set_external_editor(value, '')

    def update_actions(self):
        # This method is a required part of the PluginMainWidget API. On this
        # widget it is not currently used.
        pass

    def get_focus_widget(self):
        return self.shell

    # --- Qt overrides
    # ------------------------------------------------------------------------
    def dragEnterEvent(self, event):
        """
        Reimplement Qt method.

        Inform Qt about the types of data that the widget accepts.
        """
        source = event.mimeData()
        if source.hasUrls():
            if mimedata2url(source):
                event.acceptProposedAction()
            else:
                event.ignore()
        elif source.hasText():
            event.acceptProposedAction()

    def dropEvent(self, event):
        """
        Reimplement Qt method.

        Unpack dropped data and handle it.
        """
        source = event.mimeData()
        if source.hasUrls():
            pathlist = mimedata2url(source)
            self.shell.drop_pathlist(pathlist)
        elif source.hasText():
            lines = to_text_string(source.text())
            self.shell.set_cursor_position('eof')
            self.shell.execute_lines(lines)

        event.acceptProposedAction()

    # --- Public API
    # ------------------------------------------------------------------------
    def start_interpreter(self, namespace):
        """
        Start internal console interpreter.
        """
        self.shell.start_interpreter(namespace)

    def set_historylog(self, historylog):
        """
        Bind historylog instance to this console.

        Not used anymore since v2.0.
        """
        historylog.add_history(self.shell.history_filename)
        self.shell.append_to_history.connect(historylog.append_to_history)

    def set_help(self, help_plugin):
        """
        Bind help instance to this console.
        """
        self.shell.help = help_plugin

    def handle_exception(self, text, is_traceback, is_pyls_error=False,
                         is_faulthandler_report=False):
        """
        Exception ocurred in the internal console.

        Show a QDialog or the internal console to warn the user.
        """
        # Skip errors without traceback or dismiss
        if (not is_traceback and self.error_dlg is None) or self.dismiss_error:
            return

        if self.get_option('show_internal_errors'):
            if self.error_dlg is None:
                self.error_dlg = SpyderErrorDialog(self)
                self.error_dlg.set_color_scheme(self.get_option('color_theme'))
                self.error_dlg.close_btn.clicked.connect(self.close_error_dlg)
                self.error_dlg.rejected.connect(self.remove_error_dlg)
                self.error_dlg.details.go_to_error.connect(self.go_to_error)

            if is_pyls_error:
                title = "Internal Python Language Server error"
                self.error_dlg.set_title(title)
                self.error_dlg.title.setEnabled(False)

            if is_faulthandler_report:
                title = "Segmentation fault crash"
                self.error_dlg.set_title(title)
                self.error_dlg.title.setEnabled(False)
                self.error_dlg.main_label.setText(
                    _("<h3>Spyder crashed during last session</h3>"))
                self.error_dlg.submit_btn.setEnabled(True)
                self.error_dlg.steps_text.setText(
                    _("Please provide any additional information you "
                      "might have about the crash."))
                self.error_dlg.set_require_minimum_length(False)

            self.error_dlg.append_traceback(text)
            self.error_dlg.show()
        elif DEV or get_debug_level():
            self.change_visibility(True, True)

    def close_error_dlg(self):
        """
        Close error dialog.
        """
        if self.error_dlg.dismiss_box.isChecked():
            self.dismiss_error = True

        self.error_dlg.reject()

    def remove_error_dlg(self):
        """
        Remove error dialog.
        """
        self.error_dlg = None

    @Slot()
    def show_env(self):
        """
        Show environment variables.
        """
        self.dialog_manager.show(EnvDialog(parent=self))

    def get_sys_path(self):
        """
        Return the `sys.path`.
        """
        return sys.path

    @Slot()
    def show_syspath(self):
        """
        Show `sys.path`.
        """
        editor = CollectionsEditor(parent=self)
        editor.setup(
            sys.path,
            title="sys.path",
            readonly=True,
            icon=self.create_icon('syspath'),
        )
        self.dialog_manager.show(editor)

    @Slot()
    def run_script(self, filename=None, silent=False, set_focus=False,
                   args=None):
        """
        Run a Python script.
        """
        if filename is None:
            self.shell.interpreter.restore_stds()
            filename, _selfilter = getopenfilename(
                self,
                _("Run Python script"),
                getcwd_or_home(),
                _("Python scripts") + " (*.py ; *.pyw ; *.ipy)",
            )
            self.shell.interpreter.redirect_stds()

            if filename:
                os.chdir(osp.dirname(filename))
                filename = osp.basename(filename)
            else:
                return

        logger.debug("Running script with %s", args)
        filename = osp.abspath(filename)
        rbs = remove_backslashes
        command = "runfile('%s', args='%s')" % (rbs(filename), rbs(args))

        if set_focus:
            self.shell.setFocus()

        self.change_visibility(True, True)

        self.shell.write(command+'\n')
        self.shell.run_command(command)

    def go_to_error(self, text):
        """
        Go to error if relevant.
        """
        match = get_error_match(to_text_string(text))
        if match:
            fname, lnb = match.groups()
            self.edit_script(fname, int(lnb))

    def edit_script(self, filename=None, goto=-1):
        """
        Edit script.
        """
        if filename is not None:
            # Called from InternalShell
            self.shell.external_editor(filename, goto)
            self.sig_edit_goto_requested.emit(osp.abspath(filename), goto, '')

    def execute_lines(self, lines):
        """
        Execute lines and give focus to shell.
        """
        self.shell.execute_lines(to_text_string(lines))
        self.shell.setFocus()

    @Slot()
    def change_max_line_count(self, value=None):
        """"
        Change maximum line count.
        """
        valid = True
        if value is None:
            value, valid = QInputDialog.getInt(
                self,
                _('Buffer'),
                _('Maximum line count'),
                self.get_option('max_line_count'),
                0,
                1000000,
            )

        if valid:
            self.set_option('max_line_count', value)

    @Slot()
    def change_exteditor(self, path=None):
        """
        Change external editor path.
        """
        valid = True
        if path is None:
            path, valid = QInputDialog.getText(
                self,
                _('External editor'),
                _('External editor executable path:'),
                QLineEdit.Normal,
                self.get_option('external_editor/path'),
            )

        if valid:
            self.set_option('external_editor/path', to_text_string(path))

    def set_exit_function(self, func):
        """
        Set the callback function to execute when the `exit_interpreter` is
        called.
        """
        self.shell.exitfunc = func

    def set_font(self, font):
        """
        Set font of the internal shell.
        """
        self.shell.set_font(font)

    def redirect_stds(self):
        """
        Redirect stdout and stderr when using open file dialogs.
        """
        self.shell.interpreter.redirect_stds()

    def restore_stds(self):
        """
        Restore stdout and stderr when using open file dialogs.
        """
        self.shell.interpreter.restore_stds()

    def set_namespace_item(self, name, item):
        """
        Add an object to the namespace dictionary of the internal console.
        """
        self.shell.interpreter.namespace[name] = item

    def exit_interpreter(self):
        """
        Exit the internal console interpreter.

        This is equivalent to requesting the main application to quit.
        """
        self.shell.exit_interpreter()