# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """ Spyder preferences plugin. This plugin is in charge of managing preference pages and tabs for all plugins in Spyder, both internal and external. """ # Standard library imports import os import logging from typing import Union from packaging.version import Version from pkg_resources import parse_version # Third-party imports from qtpy.QtGui import QIcon from qtpy.QtCore import Slot from qtpy.QtWidgets import QMessageBox # Local imports from spyder.api.plugins import Plugins, SpyderPluginV2, SpyderPlugin from spyder.api.plugin_registration.decorators import on_plugin_available from spyder.config.base import _ from spyder.config.main import CONF_VERSION from spyder.config.user import NoDefault from spyder.plugins.mainmenu.api import ApplicationMenus, ToolsMenuSections from spyder.plugins.preferences.widgets.container import PreferencesContainer from spyder.plugins.toolbar.api import ApplicationToolbars, MainToolbarSections logger = logging.getLogger(__name__) BaseType = Union[int, float, bool, complex, str, bytes] IterableType = Union[list, tuple] BasicType = Union[BaseType, IterableType] class Preferences(SpyderPluginV2): """ Spyder preferences plugin. This class manages all the preference pages and tabs for all internal and external plugins, as well enabling other plugins to add configurations to other sections. """ NAME = 'preferences' CONF_SECTION = 'preferences' REQUIRES = [Plugins.Application] OPTIONAL = [Plugins.MainMenu, Plugins.Toolbar] CONF_FILE = False CONTAINER_CLASS = PreferencesContainer NEW_API = 'new' OLD_API = 'old' def __init__(self, parent, configuration=None): super().__init__(parent, configuration) self.config_pages = {} self.config_tabs = {} def register_plugin_preferences( self, plugin: Union[SpyderPluginV2, SpyderPlugin]) -> None: if (hasattr(plugin, 'CONF_WIDGET_CLASS') and plugin.CONF_WIDGET_CLASS is not None): # New API Widget = plugin.CONF_WIDGET_CLASS self.config_pages[plugin.NAME] = (self.NEW_API, Widget, plugin) plugin_conf_version = plugin.CONF_VERSION or CONF_VERSION plugin_conf_version = parse_version(plugin_conf_version) # Check if the plugin adds new configuration options to other # sections if plugin.ADDITIONAL_CONF_OPTIONS is not None: for conf_section in plugin.ADDITIONAL_CONF_OPTIONS: conf_keys = plugin.ADDITIONAL_CONF_OPTIONS[conf_section] for conf_key in conf_keys: new_value = conf_keys[conf_key] self.check_version_and_merge( conf_section, conf_key, new_value, plugin_conf_version, plugin) # Check if the plugin declares any additional configuration tabs if plugin.ADDITIONAL_CONF_TABS is not None: for plugin_name in plugin.ADDITIONAL_CONF_TABS: tabs_to_add = plugin.ADDITIONAL_CONF_TABS[plugin_name] plugin_tabs = self.config_tabs.get(plugin_name, []) plugin_tabs += tabs_to_add self.config_tabs[plugin_name] = plugin_tabs elif (hasattr(plugin, 'CONFIGWIDGET_CLASS') and plugin.CONFIGWIDGET_CLASS is not None): # Old API Widget = plugin.CONFIGWIDGET_CLASS self.config_pages[plugin.CONF_SECTION] = ( self.OLD_API, Widget, plugin) def check_version_and_merge(self, conf_section: str, conf_key: str, new_value: BasicType, current_version: Version, plugin): """Add a versioned additional option to a configuration section.""" current_value = self.get_conf( conf_key, section=conf_section, default=None) section_additional = self.get_conf('additional_configuration', section=conf_section, default={}) plugin_additional = section_additional.get(plugin.NAME, {}) if conf_key in plugin_additional: conf_key_info = plugin_additional[conf_key] prev_default = conf_key_info['default'] prev_version = parse_version(conf_key_info['version']) allow_replacement = current_version > prev_version allow_deletions = current_version.major > prev_version.major new_value = self.merge_defaults(prev_default, new_value, allow_replacement, allow_deletions) new_default = new_value if current_value != NoDefault: new_value = self.merge_configurations(current_value, new_value) self.set_conf( conf_key, new_value, section=conf_section) conf_key_info['version'] = str(current_version) conf_key_info['default'] = new_default plugin_additional[conf_key] = conf_key_info section_additional[plugin.NAME] = plugin_additional self.set_conf( 'additional_configuration', section_additional, section=conf_section) else: plugin_additional[conf_key] = { 'version': str(current_version), 'default': new_value } section_additional[plugin.NAME] = plugin_additional self.set_conf( 'additional_configuration', section_additional, section=conf_section) if current_value != NoDefault: new_value = self.merge_configurations(current_value, new_value) self.set_conf( conf_key, new_value, section=conf_section) def merge_defaults(self, prev_default: BasicType, new_default: BasicType, allow_replacement: bool = False, allow_deletions: bool = False) -> BasicType: """Compare and merge two versioned values.""" prev_type = type(prev_default) new_type = type(new_default) if prev_type is dict and new_type is dict: # Merge two dicts case for new_key in new_default: if new_key in prev_default: current_subvalue = prev_default[new_key] new_subvalue = new_default[new_key] prev_default[new_key] = self.merge_defaults( current_subvalue, new_subvalue, allow_replacement, allow_deletions) else: # Additions are allowed everytime prev_default[new_key] = new_default[new_key] if allow_deletions: for old_key in list(prev_default.keys()): if old_key not in new_default: prev_default.pop(old_key) return prev_default elif prev_default != new_default: if allow_replacement: return new_default else: return prev_default else: return prev_default def merge_configurations( self, current_value: BasicType, new_value: BasicType) -> BasicType: """ Recursively match and merge a new configuration value into a previous one. """ current_type = type(current_value) new_type = type(new_value) iterable_types = {list, tuple} base_types = {int, float, bool, complex, str, bytes} if current_type is dict and new_type is dict: # Merge two dicts case for new_key in new_value: if new_key in current_value: current_subvalue = current_value[new_key] new_subvalue = new_value[new_key] current_value[new_key] = self.merge_configurations( current_subvalue, new_subvalue) else: current_value[new_key] = new_value[new_key] return current_value elif current_type in iterable_types and new_type in iterable_types: # Merge two lists/tuples case return current_type(list(current_value) + list(new_value)) elif (current_type == new_type and current_type in base_types and new_type in base_types): # Replace the values directly return new_value elif current_type in iterable_types and new_type in base_types: # Add a value to a list or tuple return current_type((list(current_value) + [new_value])) elif current_value is None: # Assigns the new value if it doesn't exist return new_value else: logger.warning(f'The value {current_value} cannot be replaced' f'by {new_value}') return current_value def open_dialog(self, prefs_dialog_size): container = self.get_container() container.create_dialog( self.config_pages, self.config_tabs, prefs_dialog_size, self.get_main()) # ---------------- Public Spyder API required methods --------------------- def get_name(self) -> str: return _('Preferences') def get_description(self) -> str: return _('This plugin provides access to Spyder preferences page') def get_icon(self) -> QIcon: return self.create_icon('configure') def on_initialize(self): container = self.get_container() main = self.get_main() container.sig_show_preferences_requested.connect( lambda: self.open_dialog(main.prefs_dialog_size)) @on_plugin_available(plugin=Plugins.MainMenu) def on_main_menu_available(self): container = self.get_container() main_menu = self.get_plugin(Plugins.MainMenu) main_menu.add_item_to_application_menu( container.show_action, menu_id=ApplicationMenus.Tools, section=ToolsMenuSections.Tools, ) main_menu.add_item_to_application_menu( container.reset_action, menu_id=ApplicationMenus.Tools, section=ToolsMenuSections.Extras, ) @on_plugin_available(plugin=Plugins.Toolbar) def on_toolbar_available(self): container = self.get_container() toolbar = self.get_plugin(Plugins.Toolbar) toolbar.add_item_to_application_toolbar( container.show_action, toolbar_id=ApplicationToolbars.Main, section=MainToolbarSections.ApplicationSection ) @on_plugin_available(plugin=Plugins.Application) def on_application_available(self): container = self.get_container() container.sig_reset_preferences_requested.connect(self.reset) @Slot() def reset(self): answer = QMessageBox.warning(self.main, _("Warning"), _("Spyder will restart and reset to default settings:

" "Do you want to continue?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: os.environ['SPYDER_RESET'] = 'True' application = self.get_plugin(Plugins.Application) application.sig_restart_requested.emit() def unregister(self): pass def on_close(self, cancelable=False) -> bool: container = self.get_container() return not container.is_dialog_open()