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)
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]
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))
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)
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
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
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))
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)
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
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]
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)
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
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)
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)
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
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
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 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
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)
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, ) )
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
def _check_interface(self): if self.ID is None: raise SpyderAPIError("Toolbar must define an ID attribute.")
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()