Exemple #1
0
    def remove_conf(self, option, section=None):
        """
        Delete an option in the Spyder configuration system.

        Parameters
        ----------
        option: Union[str, Tuple[str, ...]]
            Name of the option, either a string or a tuple of strings.
        section: str
            Section in the configuration system.
        """
        if self._conf is not None:
            section = self.CONF_SECTION if section is None else section
            if section is None:
                raise SpyderAPIError(
                    'A spyder plugin must define a `CONF_SECTION` class '
                    'attribute!'
                )

            self._conf.remove_option(section, option)
            self.apply_conf({option}, False)
Exemple #2
0
    def get_application_toolbar(self, toolbar_id):
        """
        Return an application toolbar by toolbar_id.

        Parameters
        ----------
        toolbar_id: str
            The toolbar unique string identifier.

        Returns
        -------
        spyder.api.widgets.toolbars.ApplicationToolbar
            The application toolbar.
        """
        if toolbar_id not in self._APPLICATION_TOOLBARS:
            raise SpyderAPIError('Application toolbar "{0}" not found! '
                                 'Available toolbars are: {1}'.format(
                                     toolbar_id,
                                     list(self._APPLICATION_TOOLBARS.keys())))

        return self._APPLICATION_TOOLBARS[toolbar_id]
Exemple #3
0
    def get_plugin(self, plugin_name):
        """
        Return a plugin instance by providing the plugin's NAME.
        """
        # Ensure that this plugin has the plugin corresponding to
        # `plugin_name` listed as required or optional.
        requires = set(self.REQUIRES or [])
        optional = set(self.OPTIONAL or [])
        full_set = requires | optional

        if plugin_name in full_set or Plugins.All in full_set:
            try:
                return self._main.get_plugin(plugin_name)
            except SpyderAPIError as e:
                if plugin_name in optional:
                    return None
                else:
                    raise e
        else:
            raise SpyderAPIError('Plugin "{}" not part of REQUIRES or '
                                 'OPTIONAL requirements!'.format(plugin_name))
Exemple #4
0
    def remove_application_toolbar(self, toolbar_id: str, mainwindow=None):
        """
        Remove toolbar from application toolbars.

        Parameters
        ----------
        toolbar: str
            The application toolbar to remove from the `mainwindow`.
        mainwindow: QMainWindow
            The main application window.
        """

        if toolbar_id not in self._ADDED_TOOLBARS:
            raise SpyderAPIError(
                'Toolbar with ID "{}" is not in the main window'.format(
                    toolbar_id))

        toolbar = self._ADDED_TOOLBARS.pop(toolbar_id)
        self._toolbarslist.remove(toolbar)

        if mainwindow:
            mainwindow.removeToolBar(toolbar)
Exemple #5
0
    def create_menu(self, menu_id, title='', icon=None):
        """
        Override SpyderMenuMixin method to use a different menu class.

        Parameters
        ----------
        menu_id: str
            Unique toolbar string identifier.
        title: str
            Toolbar localized title.
        icon: QIcon or None
            Icon to use for the menu.

        Returns
        -------
        MainWidgetMenu
            The main widget menu.
        """
        menus = getattr(self, '_menus', None)
        if menus is None:
            self._menus = OrderedDict()

        if menu_id in self._menus:
            raise SpyderAPIError(
                'Menu name "{}" already in use!'.format(menu_id)
            )

        menu = MainWidgetMenu(parent=self, title=title)
        menu.ID = menu_id

        MENU_REGISTRY.register_reference(
            menu, menu.ID, self.PLUGIN_NAME, self.CONTEXT_NAME)

        if icon is not None:
            menu.menuAction().setIconVisibleInMenu(True)
            menu.setIcon(icon)

        self._menus[menu_id] = menu
        return menu
Exemple #6
0
def find_external_plugins():
    """
    Find available internal plugins based on setuptools entry points.
    """
    internal_plugins = find_internal_plugins()
    plugins = [
        entry_point for entry_point
        in pkg_resources.iter_entry_points("spyder.plugins")
    ]

    external_plugins = {}
    for entry_point in plugins:
        name = entry_point.name
        if name not in internal_plugins:
            try:
                class_name = entry_point.attrs[0]
                mod = importlib.import_module(entry_point.module_name)
                plugin_class = getattr(mod, class_name, None)

                # To display in dependencies dialog.
                # Skipped if running under test (to load boilerplate plugin)
                if not running_under_pytest():
                    plugin_class._spyder_module_name = entry_point.module_name
                    plugin_class._spyder_package_name = (
                        entry_point.dist.project_name)
                    plugin_class._spyder_version = entry_point.dist.version

                external_plugins[name] = plugin_class
                if name != plugin_class.NAME:
                    raise SpyderAPIError(
                        "Entry point name '{0}' and plugin.NAME '{1}' "
                        "do not match!".format(name, plugin_class.NAME)
                    )
            except (ModuleNotFoundError, ImportError) as error:
                print("%s: %s" % (name, str(error)), file=STDERR)
                traceback.print_exc(file=STDERR)

    return external_plugins
Exemple #7
0
    def _set_option(self, option, value, emit):
        """
        Helper method to set/change options with option to emit signal.
        """
        # Check if a togglable action exists with this name and update state
        try:
            action_name = 'toggle_{}_action'.format(option)
            self._update_action_state(action_name, value)
        except SpyderAPIError:
            pass

        self._check_options_dictionary_exist()
        if option in self.DEFAULT_OPTIONS:
            self._options[option] = value
            self.on_option_update(option, value)

            if emit:
                self.sig_option_changed.emit(option, value)
        else:
            raise SpyderAPIError(
                'Option "{}" has not been defined in the widget '
                'DEFAULT_OPTIONS attribute!'
                ''.format(option))
Exemple #8
0
    def set_conf(self,
                 option,
                 value,
                 section=None,
                 recursive_notification=True):
        """
        Set an option in Spyder configuration system.

        Parameters
        ----------
        option: str
            Name of the option (e.g. 'case_sensitive')
        value: bool, int, str, tuple, list, dict
            Value to save in the configuration system, passed as a
            Python object.
        section: str
            Section in the configuration system, e.g. `shortcuts`.
        recursive_notification: bool
            If True, all objects that observe all changes on the
            configuration section and objects that observe partial tuple paths
            are notified. For example if the option `opt` of section `sec`
            changes, then the observers for section `sec` are notified.
            Likewise, if the option `(a, b, c)` changes, then observers for
            `(a, b, c)`, `(a, b)` and a are notified as well.
        """
        if self._conf is not None:
            section = self.CONF_SECTION if section is None else section
            if section is None:
                raise SpyderAPIError(
                    'A spyder plugin must define a `CONF_SECTION` class '
                    'attribute!')

            self._conf.set(section,
                           option,
                           value,
                           recursive_notification=recursive_notification)
            self.apply_conf({option}, False)
Exemple #9
0
def find_external_plugins():
    """
    Find available internal plugins based on setuptools entry points.
    """
    internal_plugins = find_internal_plugins()
    new_plugins = [
        "appearance",
        "code_completion",
        "console",
        "core",
        "fallback_completion",
        "kite_completion",
        "lsp_completion",
        "python",
    ]
    plugins = [
        entry_point
        for entry_point in pkg_resources.iter_entry_points("spyder.plugins")
    ]

    external_plugins = {}
    for entry_point in plugins:
        name = entry_point.name
        if name not in internal_plugins and name not in new_plugins:
            try:
                class_name = entry_point.attrs[0]
                mod = importlib.import_module(entry_point.module_name)
                plugin_class = getattr(mod, class_name, None)
                external_plugins[name] = plugin_class
                if name != plugin_class.NAME:
                    raise SpyderAPIError(
                        "Entry point name and plugin.NAME do not match!")
            except (ModuleNotFoundError, ImportError) as error:
                print("%s: %s" % (name, str(error)), file=STDERR)
                traceback.print_exc(file=STDERR)

    return external_plugins
Exemple #10
0
    def get_layout(self, layout_id):
        """
        Get a registered layout by its ID.

        Parameters
        ----------
        layout_id : string
            The ID of the layout.

        Raises
        ------
        SpyderAPIError
            If the given id is not found in the registered layouts.

        Returns
        -------
        Instance of a spyder.plugins.layout.api.BaseGridLayoutType subclass
            Layout.
        """
        if layout_id not in self._spyder_layouts:
            raise SpyderAPIError(
                "Layout with id `{}` is not registered!".format(layout_id))

        return self._spyder_layouts[layout_id]
Exemple #11
0
    def add_item(self,
                 action_or_widget: ToolbarItem,
                 section: Optional[str] = None,
                 before: Optional[str] = None,
                 before_section: Optional[str] = None,
                 omit_id: bool = False):
        """
        Add action or widget item to given toolbar `section`.

        Parameters
        ----------
        item: SpyderAction or QWidget
            The item to add to the `toolbar`.
        toolbar_id: str or None
            The application toolbar unique string identifier.
        section: str or None
            The section id in which to insert the `item` on the `toolbar`.
        before: str or None
            Make the item appear before another given item.
        before_section: str or None
            Make the item defined section appear before another given section
            (must be already defined).
        omit_id: bool
            If True, then the toolbar will check if the item to add declares an
            id, False otherwise. This flag exists only for items added on
            Spyder 4 plugins. Default: False
        """
        item_id = None
        if (isinstance(action_or_widget, SpyderAction)
                or hasattr(action_or_widget, 'action_id')):
            item_id = action_or_widget.action_id
        elif hasattr(action_or_widget, 'ID'):
            item_id = action_or_widget.ID

        if not omit_id and item_id is None and action_or_widget is not None:
            raise SpyderAPIError(
                f'Item {action_or_widget} must declare an ID attribute.')

        if before is not None:
            if before not in self._item_map:
                before_pending_items = self._pending_items.get(before, [])
                before_pending_items.append(
                    (action_or_widget, section, before, before_section))
                self._pending_items[before] = before_pending_items
                return
            else:
                before = self._item_map[before]

        if section is None:
            section = self._default_section

        action_or_widget._section = section

        if before is not None:
            if section == self._default_section:
                action_or_widget._section = before._section
                section = before._section

        if section not in self._section_items:
            self._section_items[section] = [action_or_widget]
        else:
            if before is not None:
                new_actions_or_widgets = []
                for act_or_wid in self._section_items[section]:
                    if act_or_wid == before:
                        new_actions_or_widgets.append(action_or_widget)
                    new_actions_or_widgets.append(act_or_wid)

                self._section_items[section] = new_actions_or_widgets
            else:
                self._section_items[section].append(action_or_widget)
        if (before_section is not None
                and before_section in self._section_items):
            new_sections_keys = []
            for sec in self._section_items.keys():
                if sec == before_section:
                    new_sections_keys.append(section)
                if sec != section:
                    new_sections_keys.append(sec)
            self._section_items = OrderedDict(
                (section_key, self._section_items[section_key])
                for section_key in new_sections_keys)

        if item_id is not None:
            self._item_map[item_id] = action_or_widget
            if item_id in self._pending_items:
                item_pending = self._pending_items.pop(item_id)
                for item, section, before, before_section in item_pending:
                    self.add_item(item,
                                  section=section,
                                  before=before,
                                  before_section=before_section)
Exemple #12
0
def solve_plugin_dependencies(plugins):
    """
    Return a list of plugins sorted by dependencies.

    Notes
    -----
    * Prune the plugins for which required dependencies are not met
    * Prune the optional dependencies from the remaining plugins based on
        the remaining plugins available.
    * Group the remaining optional dependencies with the required
        dependencies.
    * Sort with toposort algorithm.
    """
    # Back up dependencies
    for plugin in plugins:
        if plugin.REQUIRES is None:
            plugin.REQUIRES = []

        if plugin.OPTIONAL is None:
            plugin.OPTIONAL = []

        plugin._REQUIRES = plugin.REQUIRES.copy()
        plugin._OPTIONAL = plugin.OPTIONAL.copy()

    plugin_names = {plugin.NAME: plugin for plugin in plugins}
    dependencies_dict = {}

    # Prune plugins based on required dependencies or populate the dependencies
    # if using a wildcard i.e 'Plugins.All' or to add base dependencies for
    # example the Shortcuts plugin to all SpyderDockablePlugin's (shortcut for
    # the "switch to plugin" action).
    remaining_plugins = []
    plugins_requiring_all_plugins = []
    pruning_requires = True
    import copy
    while pruning_requires:
        pruning_requires = False
        remaining_plugins = []
        current_plugins = copy.deepcopy(plugins)
        for plugin in current_plugins:
            if issubclass(plugin, (SpyderDockablePlugin, SpyderPluginWidget)):
                if Plugins.Shortcuts not in plugin.REQUIRES:
                    plugin.REQUIRES.append(Plugins.Shortcuts)
                    plugin._REQUIRES = plugin.REQUIRES.copy()
            for required in plugin.REQUIRES[:]:
                # Check self references
                if plugin.NAME == required:
                    raise SpyderAPIError("Plugin is self referencing!")

                if (required == Plugins.All and len(plugin.REQUIRES) == 1):
                    all_plugins = plugin_names.copy()
                    all_plugins.pop(plugin.NAME)
                    plugin.REQUIRES = list(all_plugins)
                    plugin._REQUIRES = plugin.REQUIRES.copy()
                    logger.info(
                        "Added all plugins as dependencies to plugin: " +
                        plugin.NAME)
                    plugins_requiring_all_plugins.append(plugin)
                    continue

                if required not in plugin_names:
                    plugin_names.pop(plugin.NAME)
                    plugins.remove(plugin)
                    for plugin_req_all in plugins_requiring_all_plugins:
                        plugin_req_all.REQUIRES = [Plugins.All]
                        plugin_req_all._REQUIRES = [Plugins.All]
                    logger.error("Pruned plugin: {}".format(plugin.NAME))
                    logger.error("Missing requirement: {}".format(required))
                    logger.error("Restart plugins pruning by REQUIRES check")
                    pruning_requires = True
                    break
            else:
                if plugin.NAME in plugin_names:
                    remaining_plugins.append(plugin)

    # Prune optional dependencies from remaining plugins
    for plugin in remaining_plugins:
        for optional in plugin.OPTIONAL:
            if optional not in plugin_names:
                plugin._OPTIONAL.remove(optional)

        plugin._REQUIRES += plugin._OPTIONAL
        dependencies_dict[plugin.NAME] = set(plugin._REQUIRES)

    # Now use toposort with plugin._REQUIRES!
    deps = toposort_flatten(dependencies_dict)

    plugin_deps = [plugin_names[name] for name in deps]

    return plugin_deps
Exemple #13
0
    def add_item_to_application_menu(self,
                                     item: ItemType,
                                     menu_id: Optional[str] = None,
                                     section: Optional[str] = None,
                                     before: Optional[str] = None,
                                     before_section: Optional[str] = None,
                                     omit_id: bool = False):
        """
        Add action or widget `item` to given application menu `section`.

        Parameters
        ----------
        item: SpyderAction or SpyderMenu
            The item to add to the `menu`.
        menu_id: str or None
            The application menu unique string identifier.
        section: str or None
            The section id in which to insert the `item` on the `menu`.
        before: str
            Make the item appear before the given object identifier.
        before_section: Section or None
            Make the item section (if provided) appear before another
            given section.
        omit_id: bool
            If True, then the menu will check if the item to add declares an
            id, False otherwise. This flag exists only for items added on
            Spyder 4 plugins. Default: False

        Notes
        -----
        Must provide a `menu` or a `menu_id`.
        """
        if not isinstance(item, (SpyderAction, SpyderMenu)) and not omit_id:
            raise SpyderAPIError('A menu only accepts items objects of type '
                                 'SpyderAction or SpyderMenu')

        # TODO: For now just add the item to the bottom for non-migrated menus.
        #       Temporal solution while migration is complete
        app_menu_actions = {
            ApplicationMenus.Edit: self._main.edit_menu_actions,
            ApplicationMenus.Search: self._main.search_menu_actions,
            ApplicationMenus.Source: self._main.source_menu_actions,
            ApplicationMenus.Run: self._main.run_menu_actions,
            ApplicationMenus.Debug: self._main.debug_menu_actions,
        }

        if menu_id in app_menu_actions:
            actions = app_menu_actions[menu_id]
            actions.append(MENU_SEPARATOR)
            actions.append(item)
        else:
            if menu_id not in self._APPLICATION_MENUS:
                pending_menu_items = self._ITEM_QUEUE.get(menu_id, [])
                pending_menu_items.append(
                    (item, section, before, before_section))
                self._ITEM_QUEUE[menu_id] = pending_menu_items
            else:
                menu = self.get_application_menu(menu_id)
                menu.add_action(item,
                                section=section,
                                before=before,
                                before_section=before_section,
                                omit_id=omit_id)
Exemple #14
0
    def remove_item_from_application_menu(self,
                                          item_id: str,
                                          menu_id: Optional[str] = None):
        """
        Remove action or widget from given application menu by id.

        Parameters
        ----------
        item_id: str
            The item identifier to remove from the given menu.
        menu_id: str or None
            The application menu unique string identifier.
        """
        if menu_id not in self._APPLICATION_MENUS:
            raise SpyderAPIError('{} is not a valid menu_id'.format(menu_id))

        # TODO: For now just add the item to the bottom for non-migrated menus.
        #       Temporal solution while migration is complete
        app_menu_actions = {
            ApplicationMenus.Edit:
            (self._main.edit_menu_actions, self._main.edit_menu),
            ApplicationMenus.Search:
            (self._main.search_menu_actions, self._main.search_menu),
            ApplicationMenus.Source:
            (self._main.source_menu_actions, self._main.source_menu),
            ApplicationMenus.Run:
            (self._main.run_menu_actions, self._main.run_menu),
            ApplicationMenus.Debug: (self._main.debug_menu_actions,
                                     self._main.debug_menu),
        }

        app_menus = {
            ApplicationMenus.Edit: self._main.edit_menu,
            ApplicationMenus.Search: self._main.search_menu,
            ApplicationMenus.Source: self._main.source_menu,
            ApplicationMenus.Run: self._main.run_menu,
            ApplicationMenus.Debug: self._main.debug_menu
        }

        menu = self.get_application_menu(menu_id)

        if menu_id in app_menu_actions:
            actions = app_menu_actions[menu_id]  # type: list
            menu = app_menus[menu_id]
            position = None
            for i, action in enumerate(actions):
                this_item_id = None
                if (isinstance(action, SpyderAction)
                        or hasattr(action, 'action_id')):
                    this_item_id = action.action_id
                elif (isinstance(action, SpyderMenu)
                      or hasattr(action, 'menu_id')):
                    this_item_id = action.menu_id
                if this_item_id is not None and this_item_id == item_id:
                    position = i
                    break
            if position is not None:
                actions.pop(position)
                menu.remove_action(item_id)
        else:
            menu.remove_action(item_id)
Exemple #15
0
    def create_action(self,
                      name,
                      text,
                      icon=None,
                      icon_text='',
                      tip=None,
                      toggled=None,
                      triggered=None,
                      shortcut_context=None,
                      context=Qt.WidgetWithChildrenShortcut,
                      initial=None,
                      register_shortcut=False,
                      section=None,
                      option=None,
                      parent=None,
                      register_action=True,
                      overwrite=False,
                      context_name=None,
                      menurole=None):
        """
        name: str
            unique identifiable name for the action
        text: str
           Localized text for the action
        icon: QIcon,
            Icon for the action when applied to menu or toolbutton.
        icon_text: str
            Icon for text in toolbars. If True, this will also disable
            the tooltip on this toolbutton if part of a toolbar.
        tip: str
            Tooltip to define for action on menu or toolbar.
        toggled: Optional[Union[Callable, bool]]
            If True, then the action modifies the configuration option on the
            section specified. Otherwise, it should be a callable to use
            when toggling this action. If None, then the action does not
            behave like a checkbox.
        triggered: callable
            The callable to use when triggering this action.
        shortcut_context: str
            Set the `str` context of the shortcut.
        context: Qt.ShortcutContext
            Set the context for the shortcut.
        initial: object
            Sets the initial state of a togglable action. This does not emit
            the toggled signal.
        section: Optional[str]
            Name of the configuration section whose option is going to be
            modified. If None, and `option` is not None, then it defaults to
            the class `CONF_SECTION` attribute.
        option: ConfigurationKey
            Name of the configuration option whose value is reflected and
            affected by the action.
        register_shortcut: bool, optional
            If True, main window will expose the shortcut in Preferences.
            The default value is `False`.
        parent: QWidget (None)
            Define the parent of the widget. Use `self` if not provided.
        register_action: bool, optional
            If True, the action will be registered and searchable.
            The default value is `True`.
        overwrite: bool, optional
            If True, in case of action overwriting no warning will be shown.
            The default value is `False`
        context_name: Optional[str]
            Name of the context that holds the action in case of registration.
            The combination of `name` and `context_name` is unique so trying
            to register an action with the same `name` and `context_name` will
            cause a warning unless `overwrite` is set to `True`.
        menurole: QAction.MenuRole, optional
            Menu role for the action (it only has effect on macOS).

        Notes
        -----
        There is no need to set shortcuts right now. We only create actions
        with this (and similar methods) and these are then exposed as possible
        shortcuts on plugin registration in the main window with the
        register_shortcut argument.

        If icon_text is True, this will also disable the tooltip.

        If a shortcut is found in the default config then it is assigned,
        otherwise it's left blank for the user to define one for it.
        """
        if triggered is None and toggled is None:
            raise SpyderAPIError(
                'Action must provide the toggled or triggered parameters!')

        if parent is None:
            parent = self

        if toggled and not callable(toggled):
            toggled = lambda value: None

        if toggled is not None:
            if section is None and option is not None:
                section = self.CONF_SECTION

        action = create_action(
            parent,
            text=text,
            icon=icon,
            tip=tip,
            toggled=toggled,
            triggered=triggered,
            context=context,
            section=section,
            option=option,
            id_=name,
            plugin=self.PLUGIN_NAME,
            context_name=(self.CONTEXT_NAME
                          if context_name is None else context_name),
            register_action=register_action,
            overwrite=overwrite,
            menurole=menurole)
        action.name = name
        if icon_text:
            action.setIconText(icon_text)

        action.text_beside_icon = bool(icon_text)
        action.shortcut_context = shortcut_context
        action.register_shortcut = register_shortcut
        action.tip = tip

        if initial is not None:
            if toggled:
                action.setChecked(initial)
            elif triggered:
                raise SpyderAPIError(
                    'Initial values can only apply to togglable actions!')
        else:
            if toggled:
                if section is not None and option is not None:
                    value = CONF.get(section, option)
                    action.setChecked(value)

        return action
Exemple #16
0
    def create_action(self,
                      name,
                      text,
                      icon=None,
                      icon_text='',
                      tip=None,
                      toggled=None,
                      triggered=None,
                      shortcut_context=None,
                      context=Qt.WidgetWithChildrenShortcut,
                      initial=None,
                      register_shortcut=False,
                      section=None,
                      option=None,
                      parent=None):
        """
        name: str
            unique identifiable name for the action
        text: str
           Localized text for the action
        icon: QIcon,
            Icon for the action when applied to menu or toolbutton.
        icon_text: str
            Icon for text in toolbars. If True, this will also disable
            the tooltip on this toolbutton if part of a toolbar.
        tip: str
            Tooltip to define for action on menu or toolbar.
        toggled: Optional[Union[Callable, bool]]
            If True, then the action modifies the configuration option on the
            section specified. Otherwise, it should be a callable to use
            when toggling this action. If None, then the action does not
            behave like a checkbox.
        triggered: callable
            The callable to use when triggering this action.
        shortcut_context: str
            Set the `str` context of the shortcut.
        context: Qt.ShortcutContext
            Set the context for the shortcut.
        initial: object
            Sets the initial state of a togglable action. This does not emit
            the toggled signal.
        section: Optional[str]
            Name of the configuration section whose option is going to be
            modified. If None, and `option` is not None, then it defaults to
            the class `CONF_SECTION` attribute.
        option: ConfigurationKey
            Name of the configuration option whose value is reflected and
            affected by the action.
        register_shortcut: bool, optional
            If True, main window will expose the shortcut in Preferences.
            The default value is `False`.
        parent: QWidget (None)
            Define the parent of the widget. Use `self` if not provided.

        Notes
        -----
        There is no need to set shortcuts right now. We only create actions
        with this (and similar methods) and these are then exposed as possible
        shortcuts on plugin registration in the main window with the
        register_shortcut argument.

        If icon_text is True, this will also disable the tooltip.

        If a shortcut is found in the default config then it is assigned,
        otherwise it's left blank for the user to define one for it.
        """
        actions = getattr(self, '_actions', None)
        if actions is None:
            self._actions = OrderedDict()

        if triggered is None and toggled is None:
            raise SpyderAPIError(
                'Action must provide the toggled or triggered parameters!')

        # Check name
        if name in self._actions:
            raise SpyderAPIError(
                'Action name "{}" already in use!'.format(name))

        if parent is None:
            parent = self

        if toggled and not callable(toggled):
            toggled = lambda value: None

        if toggled is not None:
            if section is None and option is not None:
                section = self.CONF_SECTION

        action = create_action(parent,
                               text=text,
                               icon=icon,
                               tip=tip,
                               toggled=toggled,
                               triggered=triggered,
                               context=context,
                               section=section,
                               option=option)
        action.name = name
        if icon_text:
            action.setIconText(icon_text)

        action.text_beside_icon = bool(icon_text)
        action.shortcut_context = shortcut_context
        action.register_shortcut = register_shortcut
        action.tip = tip

        if initial is not None:
            if toggled:
                action.setChecked(initial)
            elif triggered:
                raise SpyderAPIError(
                    'Initial values can only apply to togglable actions!')
        else:
            if toggled:
                if section is not None and option is not None:
                    value = CONF.get(section, option)
                    action.setChecked(value)

        self._actions[name] = action
        return action
Exemple #17
0
    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)
Exemple #18
0
    def create_action(self,
                      name,
                      text,
                      icon=None,
                      icon_text='',
                      tip=None,
                      toggled=None,
                      triggered=None,
                      shortcut_context=None,
                      context=Qt.WidgetWithChildrenShortcut,
                      initial=None,
                      register_shortcut=False,
                      parent=None):
        """
        name: str
            unique identifiable name for the action
        text: str
           Localized text for the action
        icon: QIcon,
            Icon for the action when applied to menu or toolbutton.
        icon_text: str
            Icon for text in toolbars. If True, this will also disable
            the tooltip on this toolbutton if part of a toolbar.
        tip: str
            Tooltip to define for action on menu or toolbar.
        toggled: callable
            The callable to use when toggling this action
        triggered: callable
            The callable to use when triggering this action.
        shortcut_context: str
            Set the `str` context of the shortcut.
        context: Qt.ShortcutContext
            Set the context for the shortcut.
        initial: object
            Sets the initial state of a togglable action. This does not emit
            the toggled signal.
        register_shortcut: bool, optional
            If True, main window will expose the shortcut in Preferences.
            The default value is `False`.
        parent: QWidget (None)
            Define the parent of the widget. Use `self` if not provided.

        Notes
        -----
        There is no need to set shortcuts right now. We only create actions
        with this (and similar methods) and these are then exposed as possible
        shortcuts on plugin registration in the main window with the
        register_shortcut argument.

        If icon_text is True, this will also disable the tooltip.

        If a shortcut is found in the default config then it is assigned,
        otherwise it's left blank for the user to define one for it.
        """
        actions = getattr(self, '_actions', None)
        if actions is None:
            self._actions = OrderedDict()

        if triggered is None and toggled is None:
            raise SpyderAPIError(
                'Action must provide the toggled or triggered parameters!')

        # Check name
        if name in self._actions:
            raise SpyderAPIError(
                'Action name "{}" already in use!'.format(name))

        if parent is None:
            parent = self

        action = create_action(
            parent,
            text=text,
            icon=icon,
            tip=tip,
            toggled=toggled,
            triggered=triggered,
            context=context,
        )
        action.name = name
        if icon_text:
            action.setIconText(icon_text)

        action.text_beside_icon = bool(icon_text)
        action.shortcut_context = shortcut_context
        action.register_shortcut = register_shortcut
        action.tip = tip

        if initial is not None:
            if toggled:
                self.blockSignals(True)
                action.setChecked(initial)
                self.blockSignals(False)
            elif triggered:
                raise SpyderAPIError(
                    'Initial values can only apply to togglable actions!')

        self._actions[name] = action
        return action
Exemple #19
0
    def add_item_to_application_menu(self,
                                     item,
                                     menu=None,
                                     menu_id=None,
                                     section=None,
                                     before=None,
                                     before_section=None):
        """
        Add action or widget `item` to given application menu `section`.

        Parameters
        ----------
        item: SpyderAction or SpyderMenu
            The item to add to the `menu`.
        menu: ApplicationMenu or None
            Instance of a Spyder application menu.
        menu_id: str or None
            The application menu unique string identifier.
        section: str or None
            The section id in which to insert the `item` on the `menu`.
        before: SpyderAction/SpyderMenu or None
            Make the item appear before another given item.
        before_section: Section or None
            Make the item section (if provided) appear before another
            given section.

        Notes
        -----
        Must provide a `menu` or a `menu_id`.
        """
        if menu and menu_id:
            raise SpyderAPIError('Must provide only menu or menu_id!')

        if menu is None and menu_id is None:
            raise SpyderAPIError('Must provide at least menu or menu_id!')

        if menu and not isinstance(menu, ApplicationMenu):
            raise SpyderAPIError('Not an `ApplicationMenu`!')

        if menu_id and menu_id not in self._APPLICATION_MENUS:
            raise SpyderAPIError('{} is not a valid menu_id'.format(menu_id))

        # TODO: For now just add the item to the bottom for non-migrated menus.
        #       Temporal solution while migration is complete
        app_menu_actions = {
            ApplicationMenus.File: self._main.file_menu_actions,
            ApplicationMenus.Edit: self._main.edit_menu_actions,
            ApplicationMenus.Search: self._main.search_menu_actions,
            ApplicationMenus.Source: self._main.source_menu_actions,
            ApplicationMenus.Run: self._main.run_menu_actions,
            ApplicationMenus.Debug: self._main.debug_menu_actions,
            ApplicationMenus.Consoles: self._main.consoles_menu_actions,
            ApplicationMenus.Projects: self._main.projects_menu_actions,
            ApplicationMenus.Tools: self._main.tools_menu_actions,
        }

        menu_id = menu_id if menu_id else menu.menu_id
        menu = menu if menu else self.get_application_menu(menu_id)

        if menu_id in app_menu_actions:
            actions = app_menu_actions[menu_id]
            actions.append(MENU_SEPARATOR)
            actions.append(item)
        else:
            menu.add_action(item,
                            section=section,
                            before=before,
                            before_section=before_section)
Exemple #20
0
    def add_area(self,
                 plugin_ids,
                 row,
                 column,
                 row_span=1,
                 col_span=1,
                 default=False,
                 visible=True,
                 hidden_plugin_ids=[]):
        """
        Add a new area and `plugin_ids` that will populate it to the layout.

        The area will start at row, column spanning row_pan rows and
        column_span columns.

        Parameters
        ----------
        plugin_ids: list
            List of plugin ids that will be in the area
        row: int
            Initial row where the area starts
        column: int
            Initial column where the area starts
        row_span: int, optional
            Number of rows that the area covers
        col_span: int, optional
            Number of columns the area covers
        default: bool, optiona
            Defines an area as the default one, i.e all other plugins that where
            not passed in the `plugins_ids` will be added to the default area.
            By default is False.
        visible: bool, optional
            Defines if the area is visible when setting up the layout.
            Default is True.

        Notes
        -----
        See: https://doc.qt.io/qt-5/qgridlayout.html
        """
        if self._default_added and default:
            raise SpyderAPIError("A default location has already been "
                                 "defined!")

        self._plugin_ids += plugin_ids
        self._rows = max(row, self._rows)
        self._cols = max(column, self._cols)
        self._default_added = default
        self._column_stretchs[column] = 1
        self._row_stretchs[row] = 1
        self._areas.append(
            dict(
                plugin_ids=plugin_ids,
                row=row,
                column=column,
                row_span=row_span,
                col_span=col_span,
                default=default,
                visible=visible,
                hidden_plugin_ids=hidden_plugin_ids,
            )
        )
Exemple #21
0
def solve_plugin_dependencies(plugins, testing=False):
    """
    Return a list of plugins sorted by dependencies.

    Notes
    -----
    * Prune the plugins for which required dependencies are not met
    * Prune the optional dependencies from the remaining plugins based on
        the remaining plugins available.
    * Group the remaining optional dependencies with the required
        dependencies.
    * Sort with toposort algorithm.
    """
    # Back up dependencies
    for plugin in plugins:
        if plugin.REQUIRES is None:
            plugin.REQUIRES = []

        if plugin.OPTIONAL is None:
            plugin.OPTIONAL = []

        plugin._REQUIRES = plugin.REQUIRES.copy()
        plugin._OPTIONAL = plugin.OPTIONAL.copy()

    plugin_names = {plugin.NAME: plugin for plugin in plugins}
    # TODO: Remove the next line once the migration is finished (it
    # shouldn't be necessary)
    if not testing:
        internal_plugins = find_internal_plugins()
        plugin_names.update(internal_plugins)
    dependencies_dict = {}

    # Prune plugins based on required dependencies
    remaining_plugins = []
    for plugin in plugins:
        for required in plugin.REQUIRES:
            # Check self references
            if plugin.NAME == required:
                raise SpyderAPIError("Plugin is self referencing!")

            if required not in plugin_names:
                plugin_names.pop(plugin.NAME)
                logger.error("Pruned plugin: " + plugin.NAME)
                break
        else:
            remaining_plugins.append(plugin)

    # Prune optional dependencies from remaining plugins
    for plugin in remaining_plugins:
        for optional in plugin.OPTIONAL:
            if optional not in plugin_names:
                plugin._OPTIONAL.remove(optional)

        plugin._REQUIRES += plugin._OPTIONAL
        dependencies_dict[plugin.NAME] = set(plugin._REQUIRES)

    # Now use toposort with plugin._REQUIRES!
    deps = toposort_flatten(dependencies_dict)

    # Convert back from names to plugins
    # TODO: Remove the if part when the migration is done!
    if testing:
        plugin_deps = [plugin_names[name] for name in deps]
    else:
        plugin_deps = [
            plugin_names[name] for name in deps
            if name not in internal_plugins.keys()
        ]

    return plugin_deps
Exemple #22
0
 def _check_interface(self):
     if self.ID is None:
         raise SpyderAPIError("Toolbar must define an ID attribute.")
Exemple #23
0
    def _check_layout_validity(self):
        """
        Check the current layout is a valid one.
        """
        self._visible_areas = []
        # Check ID
        if self.ID is None:
            raise SpyderAPIError("A Layout must define an `ID` class "
                                 "attribute!")

        # Check name
        self.get_name()

        # All layouts need to add at least 1 area
        if not self._areas:
            raise SpyderAPIError("A Layout must define add least one area!")

        default_areas = []
        area_zero_zero = False

        for area in self._areas:
            default_areas.append(area["default"])
            if area["default"]:
                self._default_area = area

            self._visible_areas.append(area["visible"])

            if area_zero_zero and area["row"] == 0 and area["column"] == 0:
                raise SpyderAPIError(
                    "Multiple areas defined their row and column as 0!")

            if area["row"] == 0 and area["column"] == 0:
                area_zero_zero = True

            if not set(area["hidden_plugin_ids"]) <= set(area["plugin_ids"]):
                raise SpyderAPIError(
                    "At least 1 hidden plugin id is not being specified "
                    "in the area plugin ids list!\n SpyderLayout: {}\n "
                    "hidden_plugin_ids: {}\n"
                    "plugin_ids: {}".format(self.get_name(),
                                            area["hidden_plugin_ids"],
                                            area["plugin_ids"]))

        # Check that there is at least 1 visible!
        if not any(self._visible_areas):
            raise SpyderAPIError("At least 1 area must be `visible`")

        # Check that there is a `default` area!
        if not any(default_areas):
            raise SpyderAPIError("No area is the `default`!")

        # Check that there is 1 `default` area!
        if default_areas.count(True) != 1:
            raise SpyderAPIError("Only 1 area can be the `default`!")

        # Check one area has row zero and column zero
        if not area_zero_zero:
            raise SpyderAPIError(
                "1 area needs to be specified with row 0 and column 0!")

        # Check Area
        self._check_area()