jpayne@68: """idlelib.config -- Manage IDLE configuration information. jpayne@68: jpayne@68: The comments at the beginning of config-main.def describe the jpayne@68: configuration files and the design implemented to update user jpayne@68: configuration information. In particular, user configuration choices jpayne@68: which duplicate the defaults will be removed from the user's jpayne@68: configuration files, and if a user file becomes empty, it will be jpayne@68: deleted. jpayne@68: jpayne@68: The configuration database maps options to values. Conceptually, the jpayne@68: database keys are tuples (config-type, section, item). As implemented, jpayne@68: there are separate dicts for default and user values. Each has jpayne@68: config-type keys 'main', 'extensions', 'highlight', and 'keys'. The jpayne@68: value for each key is a ConfigParser instance that maps section and item jpayne@68: to values. For 'main' and 'extensions', user values override jpayne@68: default values. For 'highlight' and 'keys', user sections augment the jpayne@68: default sections (and must, therefore, have distinct names). jpayne@68: jpayne@68: Throughout this module there is an emphasis on returning useable defaults jpayne@68: when a problem occurs in returning a requested configuration value back to jpayne@68: idle. This is to allow IDLE to continue to function in spite of errors in jpayne@68: the retrieval of config information. When a default is returned instead of jpayne@68: a requested config value, a message is printed to stderr to aid in jpayne@68: configuration problem notification and resolution. jpayne@68: """ jpayne@68: # TODOs added Oct 2014, tjr jpayne@68: jpayne@68: from configparser import ConfigParser jpayne@68: import os jpayne@68: import sys jpayne@68: jpayne@68: from tkinter.font import Font jpayne@68: import idlelib jpayne@68: jpayne@68: class InvalidConfigType(Exception): pass jpayne@68: class InvalidConfigSet(Exception): pass jpayne@68: class InvalidTheme(Exception): pass jpayne@68: jpayne@68: class IdleConfParser(ConfigParser): jpayne@68: """ jpayne@68: A ConfigParser specialised for idle configuration file handling jpayne@68: """ jpayne@68: def __init__(self, cfgFile, cfgDefaults=None): jpayne@68: """ jpayne@68: cfgFile - string, fully specified configuration file name jpayne@68: """ jpayne@68: self.file = cfgFile # This is currently '' when testing. jpayne@68: ConfigParser.__init__(self, defaults=cfgDefaults, strict=False) jpayne@68: jpayne@68: def Get(self, section, option, type=None, default=None, raw=False): jpayne@68: """ jpayne@68: Get an option value for given section/option or return default. jpayne@68: If type is specified, return as type. jpayne@68: """ jpayne@68: # TODO Use default as fallback, at least if not None jpayne@68: # Should also print Warning(file, section, option). jpayne@68: # Currently may raise ValueError jpayne@68: if not self.has_option(section, option): jpayne@68: return default jpayne@68: if type == 'bool': jpayne@68: return self.getboolean(section, option) jpayne@68: elif type == 'int': jpayne@68: return self.getint(section, option) jpayne@68: else: jpayne@68: return self.get(section, option, raw=raw) jpayne@68: jpayne@68: def GetOptionList(self, section): jpayne@68: "Return a list of options for given section, else []." jpayne@68: if self.has_section(section): jpayne@68: return self.options(section) jpayne@68: else: #return a default value jpayne@68: return [] jpayne@68: jpayne@68: def Load(self): jpayne@68: "Load the configuration file from disk." jpayne@68: if self.file: jpayne@68: self.read(self.file) jpayne@68: jpayne@68: class IdleUserConfParser(IdleConfParser): jpayne@68: """ jpayne@68: IdleConfigParser specialised for user configuration handling. jpayne@68: """ jpayne@68: jpayne@68: def SetOption(self, section, option, value): jpayne@68: """Return True if option is added or changed to value, else False. jpayne@68: jpayne@68: Add section if required. False means option already had value. jpayne@68: """ jpayne@68: if self.has_option(section, option): jpayne@68: if self.get(section, option) == value: jpayne@68: return False jpayne@68: else: jpayne@68: self.set(section, option, value) jpayne@68: return True jpayne@68: else: jpayne@68: if not self.has_section(section): jpayne@68: self.add_section(section) jpayne@68: self.set(section, option, value) jpayne@68: return True jpayne@68: jpayne@68: def RemoveOption(self, section, option): jpayne@68: """Return True if option is removed from section, else False. jpayne@68: jpayne@68: False if either section does not exist or did not have option. jpayne@68: """ jpayne@68: if self.has_section(section): jpayne@68: return self.remove_option(section, option) jpayne@68: return False jpayne@68: jpayne@68: def AddSection(self, section): jpayne@68: "If section doesn't exist, add it." jpayne@68: if not self.has_section(section): jpayne@68: self.add_section(section) jpayne@68: jpayne@68: def RemoveEmptySections(self): jpayne@68: "Remove any sections that have no options." jpayne@68: for section in self.sections(): jpayne@68: if not self.GetOptionList(section): jpayne@68: self.remove_section(section) jpayne@68: jpayne@68: def IsEmpty(self): jpayne@68: "Return True if no sections after removing empty sections." jpayne@68: self.RemoveEmptySections() jpayne@68: return not self.sections() jpayne@68: jpayne@68: def Save(self): jpayne@68: """Update user configuration file. jpayne@68: jpayne@68: If self not empty after removing empty sections, write the file jpayne@68: to disk. Otherwise, remove the file from disk if it exists. jpayne@68: """ jpayne@68: fname = self.file jpayne@68: if fname and fname[0] != '#': jpayne@68: if not self.IsEmpty(): jpayne@68: try: jpayne@68: cfgFile = open(fname, 'w') jpayne@68: except OSError: jpayne@68: os.unlink(fname) jpayne@68: cfgFile = open(fname, 'w') jpayne@68: with cfgFile: jpayne@68: self.write(cfgFile) jpayne@68: elif os.path.exists(self.file): jpayne@68: os.remove(self.file) jpayne@68: jpayne@68: class IdleConf: jpayne@68: """Hold config parsers for all idle config files in singleton instance. jpayne@68: jpayne@68: Default config files, self.defaultCfg -- jpayne@68: for config_type in self.config_types: jpayne@68: (idle install dir)/config-{config-type}.def jpayne@68: jpayne@68: User config files, self.userCfg -- jpayne@68: for config_type in self.config_types: jpayne@68: (user home dir)/.idlerc/config-{config-type}.cfg jpayne@68: """ jpayne@68: def __init__(self, _utest=False): jpayne@68: self.config_types = ('main', 'highlight', 'keys', 'extensions') jpayne@68: self.defaultCfg = {} jpayne@68: self.userCfg = {} jpayne@68: self.cfg = {} # TODO use to select userCfg vs defaultCfg jpayne@68: # self.blink_off_time = ['insertofftime'] jpayne@68: # See https:/bugs.python.org/issue4630, msg356516. jpayne@68: jpayne@68: if not _utest: jpayne@68: self.CreateConfigHandlers() jpayne@68: self.LoadCfgFiles() jpayne@68: jpayne@68: def CreateConfigHandlers(self): jpayne@68: "Populate default and user config parser dictionaries." jpayne@68: idledir = os.path.dirname(__file__) jpayne@68: self.userdir = userdir = '' if idlelib.testing else self.GetUserCfgDir() jpayne@68: for cfg_type in self.config_types: jpayne@68: self.defaultCfg[cfg_type] = IdleConfParser( jpayne@68: os.path.join(idledir, f'config-{cfg_type}.def')) jpayne@68: self.userCfg[cfg_type] = IdleUserConfParser( jpayne@68: os.path.join(userdir or '#', f'config-{cfg_type}.cfg')) jpayne@68: jpayne@68: def GetUserCfgDir(self): jpayne@68: """Return a filesystem directory for storing user config files. jpayne@68: jpayne@68: Creates it if required. jpayne@68: """ jpayne@68: cfgDir = '.idlerc' jpayne@68: userDir = os.path.expanduser('~') jpayne@68: if userDir != '~': # expanduser() found user home dir jpayne@68: if not os.path.exists(userDir): jpayne@68: if not idlelib.testing: jpayne@68: warn = ('\n Warning: os.path.expanduser("~") points to\n ' + jpayne@68: userDir + ',\n but the path does not exist.') jpayne@68: try: jpayne@68: print(warn, file=sys.stderr) jpayne@68: except OSError: jpayne@68: pass jpayne@68: userDir = '~' jpayne@68: if userDir == "~": # still no path to home! jpayne@68: # traditionally IDLE has defaulted to os.getcwd(), is this adequate? jpayne@68: userDir = os.getcwd() jpayne@68: userDir = os.path.join(userDir, cfgDir) jpayne@68: if not os.path.exists(userDir): jpayne@68: try: jpayne@68: os.mkdir(userDir) jpayne@68: except OSError: jpayne@68: if not idlelib.testing: jpayne@68: warn = ('\n Warning: unable to create user config directory\n' + jpayne@68: userDir + '\n Check path and permissions.\n Exiting!\n') jpayne@68: try: jpayne@68: print(warn, file=sys.stderr) jpayne@68: except OSError: jpayne@68: pass jpayne@68: raise SystemExit jpayne@68: # TODO continue without userDIr instead of exit jpayne@68: return userDir jpayne@68: jpayne@68: def GetOption(self, configType, section, option, default=None, type=None, jpayne@68: warn_on_default=True, raw=False): jpayne@68: """Return a value for configType section option, or default. jpayne@68: jpayne@68: If type is not None, return a value of that type. Also pass raw jpayne@68: to the config parser. First try to return a valid value jpayne@68: (including type) from a user configuration. If that fails, try jpayne@68: the default configuration. If that fails, return default, with a jpayne@68: default of None. jpayne@68: jpayne@68: Warn if either user or default configurations have an invalid value. jpayne@68: Warn if default is returned and warn_on_default is True. jpayne@68: """ jpayne@68: try: jpayne@68: if self.userCfg[configType].has_option(section, option): jpayne@68: return self.userCfg[configType].Get(section, option, jpayne@68: type=type, raw=raw) jpayne@68: except ValueError: jpayne@68: warning = ('\n Warning: config.py - IdleConf.GetOption -\n' jpayne@68: ' invalid %r value for configuration option %r\n' jpayne@68: ' from section %r: %r' % jpayne@68: (type, option, section, jpayne@68: self.userCfg[configType].Get(section, option, raw=raw))) jpayne@68: _warn(warning, configType, section, option) jpayne@68: try: jpayne@68: if self.defaultCfg[configType].has_option(section,option): jpayne@68: return self.defaultCfg[configType].Get( jpayne@68: section, option, type=type, raw=raw) jpayne@68: except ValueError: jpayne@68: pass jpayne@68: #returning default, print warning jpayne@68: if warn_on_default: jpayne@68: warning = ('\n Warning: config.py - IdleConf.GetOption -\n' jpayne@68: ' problem retrieving configuration option %r\n' jpayne@68: ' from section %r.\n' jpayne@68: ' returning default value: %r' % jpayne@68: (option, section, default)) jpayne@68: _warn(warning, configType, section, option) jpayne@68: return default jpayne@68: jpayne@68: def SetOption(self, configType, section, option, value): jpayne@68: """Set section option to value in user config file.""" jpayne@68: self.userCfg[configType].SetOption(section, option, value) jpayne@68: jpayne@68: def GetSectionList(self, configSet, configType): jpayne@68: """Return sections for configSet configType configuration. jpayne@68: jpayne@68: configSet must be either 'user' or 'default' jpayne@68: configType must be in self.config_types. jpayne@68: """ jpayne@68: if not (configType in self.config_types): jpayne@68: raise InvalidConfigType('Invalid configType specified') jpayne@68: if configSet == 'user': jpayne@68: cfgParser = self.userCfg[configType] jpayne@68: elif configSet == 'default': jpayne@68: cfgParser=self.defaultCfg[configType] jpayne@68: else: jpayne@68: raise InvalidConfigSet('Invalid configSet specified') jpayne@68: return cfgParser.sections() jpayne@68: jpayne@68: def GetHighlight(self, theme, element): jpayne@68: """Return dict of theme element highlight colors. jpayne@68: jpayne@68: The keys are 'foreground' and 'background'. The values are jpayne@68: tkinter color strings for configuring backgrounds and tags. jpayne@68: """ jpayne@68: cfg = ('default' if self.defaultCfg['highlight'].has_section(theme) jpayne@68: else 'user') jpayne@68: theme_dict = self.GetThemeDict(cfg, theme) jpayne@68: fore = theme_dict[element + '-foreground'] jpayne@68: if element == 'cursor': jpayne@68: element = 'normal' jpayne@68: back = theme_dict[element + '-background'] jpayne@68: return {"foreground": fore, "background": back} jpayne@68: jpayne@68: def GetThemeDict(self, type, themeName): jpayne@68: """Return {option:value} dict for elements in themeName. jpayne@68: jpayne@68: type - string, 'default' or 'user' theme type jpayne@68: themeName - string, theme name jpayne@68: Values are loaded over ultimate fallback defaults to guarantee jpayne@68: that all theme elements are present in a newly created theme. jpayne@68: """ jpayne@68: if type == 'user': jpayne@68: cfgParser = self.userCfg['highlight'] jpayne@68: elif type == 'default': jpayne@68: cfgParser = self.defaultCfg['highlight'] jpayne@68: else: jpayne@68: raise InvalidTheme('Invalid theme type specified') jpayne@68: # Provide foreground and background colors for each theme jpayne@68: # element (other than cursor) even though some values are not jpayne@68: # yet used by idle, to allow for their use in the future. jpayne@68: # Default values are generally black and white. jpayne@68: # TODO copy theme from a class attribute. jpayne@68: theme ={'normal-foreground':'#000000', jpayne@68: 'normal-background':'#ffffff', jpayne@68: 'keyword-foreground':'#000000', jpayne@68: 'keyword-background':'#ffffff', jpayne@68: 'builtin-foreground':'#000000', jpayne@68: 'builtin-background':'#ffffff', jpayne@68: 'comment-foreground':'#000000', jpayne@68: 'comment-background':'#ffffff', jpayne@68: 'string-foreground':'#000000', jpayne@68: 'string-background':'#ffffff', jpayne@68: 'definition-foreground':'#000000', jpayne@68: 'definition-background':'#ffffff', jpayne@68: 'hilite-foreground':'#000000', jpayne@68: 'hilite-background':'gray', jpayne@68: 'break-foreground':'#ffffff', jpayne@68: 'break-background':'#000000', jpayne@68: 'hit-foreground':'#ffffff', jpayne@68: 'hit-background':'#000000', jpayne@68: 'error-foreground':'#ffffff', jpayne@68: 'error-background':'#000000', jpayne@68: 'context-foreground':'#000000', jpayne@68: 'context-background':'#ffffff', jpayne@68: 'linenumber-foreground':'#000000', jpayne@68: 'linenumber-background':'#ffffff', jpayne@68: #cursor (only foreground can be set) jpayne@68: 'cursor-foreground':'#000000', jpayne@68: #shell window jpayne@68: 'stdout-foreground':'#000000', jpayne@68: 'stdout-background':'#ffffff', jpayne@68: 'stderr-foreground':'#000000', jpayne@68: 'stderr-background':'#ffffff', jpayne@68: 'console-foreground':'#000000', jpayne@68: 'console-background':'#ffffff', jpayne@68: } jpayne@68: for element in theme: jpayne@68: if not (cfgParser.has_option(themeName, element) or jpayne@68: # Skip warning for new elements. jpayne@68: element.startswith(('context-', 'linenumber-'))): jpayne@68: # Print warning that will return a default color jpayne@68: warning = ('\n Warning: config.IdleConf.GetThemeDict' jpayne@68: ' -\n problem retrieving theme element %r' jpayne@68: '\n from theme %r.\n' jpayne@68: ' returning default color: %r' % jpayne@68: (element, themeName, theme[element])) jpayne@68: _warn(warning, 'highlight', themeName, element) jpayne@68: theme[element] = cfgParser.Get( jpayne@68: themeName, element, default=theme[element]) jpayne@68: return theme jpayne@68: jpayne@68: def CurrentTheme(self): jpayne@68: "Return the name of the currently active text color theme." jpayne@68: return self.current_colors_and_keys('Theme') jpayne@68: jpayne@68: def CurrentKeys(self): jpayne@68: """Return the name of the currently active key set.""" jpayne@68: return self.current_colors_and_keys('Keys') jpayne@68: jpayne@68: def current_colors_and_keys(self, section): jpayne@68: """Return the currently active name for Theme or Keys section. jpayne@68: jpayne@68: idlelib.config-main.def ('default') includes these sections jpayne@68: jpayne@68: [Theme] jpayne@68: default= 1 jpayne@68: name= IDLE Classic jpayne@68: name2= jpayne@68: jpayne@68: [Keys] jpayne@68: default= 1 jpayne@68: name= jpayne@68: name2= jpayne@68: jpayne@68: Item 'name2', is used for built-in ('default') themes and keys jpayne@68: added after 2015 Oct 1 and 2016 July 1. This kludge is needed jpayne@68: because setting 'name' to a builtin not defined in older IDLEs jpayne@68: to display multiple error messages or quit. jpayne@68: See https://bugs.python.org/issue25313. jpayne@68: When default = True, 'name2' takes precedence over 'name', jpayne@68: while older IDLEs will just use name. When default = False, jpayne@68: 'name2' may still be set, but it is ignored. jpayne@68: """ jpayne@68: cfgname = 'highlight' if section == 'Theme' else 'keys' jpayne@68: default = self.GetOption('main', section, 'default', jpayne@68: type='bool', default=True) jpayne@68: name = '' jpayne@68: if default: jpayne@68: name = self.GetOption('main', section, 'name2', default='') jpayne@68: if not name: jpayne@68: name = self.GetOption('main', section, 'name', default='') jpayne@68: if name: jpayne@68: source = self.defaultCfg if default else self.userCfg jpayne@68: if source[cfgname].has_section(name): jpayne@68: return name jpayne@68: return "IDLE Classic" if section == 'Theme' else self.default_keys() jpayne@68: jpayne@68: @staticmethod jpayne@68: def default_keys(): jpayne@68: if sys.platform[:3] == 'win': jpayne@68: return 'IDLE Classic Windows' jpayne@68: elif sys.platform == 'darwin': jpayne@68: return 'IDLE Classic OSX' jpayne@68: else: jpayne@68: return 'IDLE Modern Unix' jpayne@68: jpayne@68: def GetExtensions(self, active_only=True, jpayne@68: editor_only=False, shell_only=False): jpayne@68: """Return extensions in default and user config-extensions files. jpayne@68: jpayne@68: If active_only True, only return active (enabled) extensions jpayne@68: and optionally only editor or shell extensions. jpayne@68: If active_only False, return all extensions. jpayne@68: """ jpayne@68: extns = self.RemoveKeyBindNames( jpayne@68: self.GetSectionList('default', 'extensions')) jpayne@68: userExtns = self.RemoveKeyBindNames( jpayne@68: self.GetSectionList('user', 'extensions')) jpayne@68: for extn in userExtns: jpayne@68: if extn not in extns: #user has added own extension jpayne@68: extns.append(extn) jpayne@68: for extn in ('AutoComplete','CodeContext', jpayne@68: 'FormatParagraph','ParenMatch'): jpayne@68: extns.remove(extn) jpayne@68: # specific exclusions because we are storing config for mainlined old jpayne@68: # extensions in config-extensions.def for backward compatibility jpayne@68: if active_only: jpayne@68: activeExtns = [] jpayne@68: for extn in extns: jpayne@68: if self.GetOption('extensions', extn, 'enable', default=True, jpayne@68: type='bool'): jpayne@68: #the extension is enabled jpayne@68: if editor_only or shell_only: # TODO both True contradict jpayne@68: if editor_only: jpayne@68: option = "enable_editor" jpayne@68: else: jpayne@68: option = "enable_shell" jpayne@68: if self.GetOption('extensions', extn,option, jpayne@68: default=True, type='bool', jpayne@68: warn_on_default=False): jpayne@68: activeExtns.append(extn) jpayne@68: else: jpayne@68: activeExtns.append(extn) jpayne@68: return activeExtns jpayne@68: else: jpayne@68: return extns jpayne@68: jpayne@68: def RemoveKeyBindNames(self, extnNameList): jpayne@68: "Return extnNameList with keybinding section names removed." jpayne@68: return [n for n in extnNameList if not n.endswith(('_bindings', '_cfgBindings'))] jpayne@68: jpayne@68: def GetExtnNameForEvent(self, virtualEvent): jpayne@68: """Return the name of the extension binding virtualEvent, or None. jpayne@68: jpayne@68: virtualEvent - string, name of the virtual event to test for, jpayne@68: without the enclosing '<< >>' jpayne@68: """ jpayne@68: extName = None jpayne@68: vEvent = '<<' + virtualEvent + '>>' jpayne@68: for extn in self.GetExtensions(active_only=0): jpayne@68: for event in self.GetExtensionKeys(extn): jpayne@68: if event == vEvent: jpayne@68: extName = extn # TODO return here? jpayne@68: return extName jpayne@68: jpayne@68: def GetExtensionKeys(self, extensionName): jpayne@68: """Return dict: {configurable extensionName event : active keybinding}. jpayne@68: jpayne@68: Events come from default config extension_cfgBindings section. jpayne@68: Keybindings come from GetCurrentKeySet() active key dict, jpayne@68: where previously used bindings are disabled. jpayne@68: """ jpayne@68: keysName = extensionName + '_cfgBindings' jpayne@68: activeKeys = self.GetCurrentKeySet() jpayne@68: extKeys = {} jpayne@68: if self.defaultCfg['extensions'].has_section(keysName): jpayne@68: eventNames = self.defaultCfg['extensions'].GetOptionList(keysName) jpayne@68: for eventName in eventNames: jpayne@68: event = '<<' + eventName + '>>' jpayne@68: binding = activeKeys[event] jpayne@68: extKeys[event] = binding jpayne@68: return extKeys jpayne@68: jpayne@68: def __GetRawExtensionKeys(self,extensionName): jpayne@68: """Return dict {configurable extensionName event : keybinding list}. jpayne@68: jpayne@68: Events come from default config extension_cfgBindings section. jpayne@68: Keybindings list come from the splitting of GetOption, which jpayne@68: tries user config before default config. jpayne@68: """ jpayne@68: keysName = extensionName+'_cfgBindings' jpayne@68: extKeys = {} jpayne@68: if self.defaultCfg['extensions'].has_section(keysName): jpayne@68: eventNames = self.defaultCfg['extensions'].GetOptionList(keysName) jpayne@68: for eventName in eventNames: jpayne@68: binding = self.GetOption( jpayne@68: 'extensions', keysName, eventName, default='').split() jpayne@68: event = '<<' + eventName + '>>' jpayne@68: extKeys[event] = binding jpayne@68: return extKeys jpayne@68: jpayne@68: def GetExtensionBindings(self, extensionName): jpayne@68: """Return dict {extensionName event : active or defined keybinding}. jpayne@68: jpayne@68: Augment self.GetExtensionKeys(extensionName) with mapping of non- jpayne@68: configurable events (from default config) to GetOption splits, jpayne@68: as in self.__GetRawExtensionKeys. jpayne@68: """ jpayne@68: bindsName = extensionName + '_bindings' jpayne@68: extBinds = self.GetExtensionKeys(extensionName) jpayne@68: #add the non-configurable bindings jpayne@68: if self.defaultCfg['extensions'].has_section(bindsName): jpayne@68: eventNames = self.defaultCfg['extensions'].GetOptionList(bindsName) jpayne@68: for eventName in eventNames: jpayne@68: binding = self.GetOption( jpayne@68: 'extensions', bindsName, eventName, default='').split() jpayne@68: event = '<<' + eventName + '>>' jpayne@68: extBinds[event] = binding jpayne@68: jpayne@68: return extBinds jpayne@68: jpayne@68: def GetKeyBinding(self, keySetName, eventStr): jpayne@68: """Return the keybinding list for keySetName eventStr. jpayne@68: jpayne@68: keySetName - name of key binding set (config-keys section). jpayne@68: eventStr - virtual event, including brackets, as in '<>'. jpayne@68: """ jpayne@68: eventName = eventStr[2:-2] #trim off the angle brackets jpayne@68: binding = self.GetOption('keys', keySetName, eventName, default='', jpayne@68: warn_on_default=False).split() jpayne@68: return binding jpayne@68: jpayne@68: def GetCurrentKeySet(self): jpayne@68: "Return CurrentKeys with 'darwin' modifications." jpayne@68: result = self.GetKeySet(self.CurrentKeys()) jpayne@68: jpayne@68: if sys.platform == "darwin": jpayne@68: # macOS (OS X) Tk variants do not support the "Alt" jpayne@68: # keyboard modifier. Replace it with "Option". jpayne@68: # TODO (Ned?): the "Option" modifier does not work properly jpayne@68: # for Cocoa Tk and XQuartz Tk so we should not use it jpayne@68: # in the default 'OSX' keyset. jpayne@68: for k, v in result.items(): jpayne@68: v2 = [ x.replace('>' jpayne@68: """ jpayne@68: return ('<<'+virtualEvent+'>>') in self.GetCoreKeys() jpayne@68: jpayne@68: # TODO make keyBindins a file or class attribute used for test above jpayne@68: # and copied in function below. jpayne@68: jpayne@68: former_extension_events = { # Those with user-configurable keys. jpayne@68: '<>', '<>', jpayne@68: '<>', '<>', '<>', jpayne@68: '<>', '<>', '<>', jpayne@68: '<>', jpayne@68: } jpayne@68: jpayne@68: def GetCoreKeys(self, keySetName=None): jpayne@68: """Return dict of core virtual-key keybindings for keySetName. jpayne@68: jpayne@68: The default keySetName None corresponds to the keyBindings base jpayne@68: dict. If keySetName is not None, bindings from the config jpayne@68: file(s) are loaded _over_ these defaults, so if there is a jpayne@68: problem getting any core binding there will be an 'ultimate last jpayne@68: resort fallback' to the CUA-ish bindings defined here. jpayne@68: """ jpayne@68: keyBindings={ jpayne@68: '<>': ['', ''], jpayne@68: '<>': ['', ''], jpayne@68: '<>': ['', ''], jpayne@68: '<>': ['', ''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': ['', ''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': ['', ''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: '<>': [''], jpayne@68: } jpayne@68: jpayne@68: if keySetName: jpayne@68: if not (self.userCfg['keys'].has_section(keySetName) or jpayne@68: self.defaultCfg['keys'].has_section(keySetName)): jpayne@68: warning = ( jpayne@68: '\n Warning: config.py - IdleConf.GetCoreKeys -\n' jpayne@68: ' key set %r is not defined, using default bindings.' % jpayne@68: (keySetName,) jpayne@68: ) jpayne@68: _warn(warning, 'keys', keySetName) jpayne@68: else: jpayne@68: for event in keyBindings: jpayne@68: binding = self.GetKeyBinding(keySetName, event) jpayne@68: if binding: jpayne@68: keyBindings[event] = binding jpayne@68: # Otherwise return default in keyBindings. jpayne@68: elif event not in self.former_extension_events: jpayne@68: warning = ( jpayne@68: '\n Warning: config.py - IdleConf.GetCoreKeys -\n' jpayne@68: ' problem retrieving key binding for event %r\n' jpayne@68: ' from key set %r.\n' jpayne@68: ' returning default value: %r' % jpayne@68: (event, keySetName, keyBindings[event]) jpayne@68: ) jpayne@68: _warn(warning, 'keys', keySetName, event) jpayne@68: return keyBindings jpayne@68: jpayne@68: def GetExtraHelpSourceList(self, configSet): jpayne@68: """Return list of extra help sources from a given configSet. jpayne@68: jpayne@68: Valid configSets are 'user' or 'default'. Return a list of tuples of jpayne@68: the form (menu_item , path_to_help_file , option), or return the empty jpayne@68: list. 'option' is the sequence number of the help resource. 'option' jpayne@68: values determine the position of the menu items on the Help menu, jpayne@68: therefore the returned list must be sorted by 'option'. jpayne@68: jpayne@68: """ jpayne@68: helpSources = [] jpayne@68: if configSet == 'user': jpayne@68: cfgParser = self.userCfg['main'] jpayne@68: elif configSet == 'default': jpayne@68: cfgParser = self.defaultCfg['main'] jpayne@68: else: jpayne@68: raise InvalidConfigSet('Invalid configSet specified') jpayne@68: options=cfgParser.GetOptionList('HelpFiles') jpayne@68: for option in options: jpayne@68: value=cfgParser.Get('HelpFiles', option, default=';') jpayne@68: if value.find(';') == -1: #malformed config entry with no ';' jpayne@68: menuItem = '' #make these empty jpayne@68: helpPath = '' #so value won't be added to list jpayne@68: else: #config entry contains ';' as expected jpayne@68: value=value.split(';') jpayne@68: menuItem=value[0].strip() jpayne@68: helpPath=value[1].strip() jpayne@68: if menuItem and helpPath: #neither are empty strings jpayne@68: helpSources.append( (menuItem,helpPath,option) ) jpayne@68: helpSources.sort(key=lambda x: x[2]) jpayne@68: return helpSources jpayne@68: jpayne@68: def GetAllExtraHelpSourcesList(self): jpayne@68: """Return a list of the details of all additional help sources. jpayne@68: jpayne@68: Tuples in the list are those of GetExtraHelpSourceList. jpayne@68: """ jpayne@68: allHelpSources = (self.GetExtraHelpSourceList('default') + jpayne@68: self.GetExtraHelpSourceList('user') ) jpayne@68: return allHelpSources jpayne@68: jpayne@68: def GetFont(self, root, configType, section): jpayne@68: """Retrieve a font from configuration (font, font-size, font-bold) jpayne@68: Intercept the special value 'TkFixedFont' and substitute jpayne@68: the actual font, factoring in some tweaks if needed for jpayne@68: appearance sakes. jpayne@68: jpayne@68: The 'root' parameter can normally be any valid Tkinter widget. jpayne@68: jpayne@68: Return a tuple (family, size, weight) suitable for passing jpayne@68: to tkinter.Font jpayne@68: """ jpayne@68: family = self.GetOption(configType, section, 'font', default='courier') jpayne@68: size = self.GetOption(configType, section, 'font-size', type='int', jpayne@68: default='10') jpayne@68: bold = self.GetOption(configType, section, 'font-bold', default=0, jpayne@68: type='bool') jpayne@68: if (family == 'TkFixedFont'): jpayne@68: f = Font(name='TkFixedFont', exists=True, root=root) jpayne@68: actualFont = Font.actual(f) jpayne@68: family = actualFont['family'] jpayne@68: size = actualFont['size'] jpayne@68: if size <= 0: jpayne@68: size = 10 # if font in pixels, ignore actual size jpayne@68: bold = actualFont['weight'] == 'bold' jpayne@68: return (family, size, 'bold' if bold else 'normal') jpayne@68: jpayne@68: def LoadCfgFiles(self): jpayne@68: "Load all configuration files." jpayne@68: for key in self.defaultCfg: jpayne@68: self.defaultCfg[key].Load() jpayne@68: self.userCfg[key].Load() #same keys jpayne@68: jpayne@68: def SaveUserCfgFiles(self): jpayne@68: "Write all loaded user configuration files to disk." jpayne@68: for key in self.userCfg: jpayne@68: self.userCfg[key].Save() jpayne@68: jpayne@68: jpayne@68: idleConf = IdleConf() jpayne@68: jpayne@68: _warned = set() jpayne@68: def _warn(msg, *key): jpayne@68: key = (msg,) + key jpayne@68: if key not in _warned: jpayne@68: try: jpayne@68: print(msg, file=sys.stderr) jpayne@68: except OSError: jpayne@68: pass jpayne@68: _warned.add(key) jpayne@68: jpayne@68: jpayne@68: class ConfigChanges(dict): jpayne@68: """Manage a user's proposed configuration option changes. jpayne@68: jpayne@68: Names used across multiple methods: jpayne@68: page -- one of the 4 top-level dicts representing a jpayne@68: .idlerc/config-x.cfg file. jpayne@68: config_type -- name of a page. jpayne@68: section -- a section within a page/file. jpayne@68: option -- name of an option within a section. jpayne@68: value -- value for the option. jpayne@68: jpayne@68: Methods jpayne@68: add_option: Add option and value to changes. jpayne@68: save_option: Save option and value to config parser. jpayne@68: save_all: Save all the changes to the config parser and file. jpayne@68: delete_section: If section exists, jpayne@68: delete from changes, userCfg, and file. jpayne@68: clear: Clear all changes by clearing each page. jpayne@68: """ jpayne@68: def __init__(self): jpayne@68: "Create a page for each configuration file" jpayne@68: self.pages = [] # List of unhashable dicts. jpayne@68: for config_type in idleConf.config_types: jpayne@68: self[config_type] = {} jpayne@68: self.pages.append(self[config_type]) jpayne@68: jpayne@68: def add_option(self, config_type, section, item, value): jpayne@68: "Add item/value pair for config_type and section." jpayne@68: page = self[config_type] jpayne@68: value = str(value) # Make sure we use a string. jpayne@68: if section not in page: jpayne@68: page[section] = {} jpayne@68: page[section][item] = value jpayne@68: jpayne@68: @staticmethod jpayne@68: def save_option(config_type, section, item, value): jpayne@68: """Return True if the configuration value was added or changed. jpayne@68: jpayne@68: Helper for save_all. jpayne@68: """ jpayne@68: if idleConf.defaultCfg[config_type].has_option(section, item): jpayne@68: if idleConf.defaultCfg[config_type].Get(section, item) == value: jpayne@68: # The setting equals a default setting, remove it from user cfg. jpayne@68: return idleConf.userCfg[config_type].RemoveOption(section, item) jpayne@68: # If we got here, set the option. jpayne@68: return idleConf.userCfg[config_type].SetOption(section, item, value) jpayne@68: jpayne@68: def save_all(self): jpayne@68: """Save configuration changes to the user config file. jpayne@68: jpayne@68: Clear self in preparation for additional changes. jpayne@68: Return changed for testing. jpayne@68: """ jpayne@68: idleConf.userCfg['main'].Save() jpayne@68: jpayne@68: changed = False jpayne@68: for config_type in self: jpayne@68: cfg_type_changed = False jpayne@68: page = self[config_type] jpayne@68: for section in page: jpayne@68: if section == 'HelpFiles': # Remove it for replacement. jpayne@68: idleConf.userCfg['main'].remove_section('HelpFiles') jpayne@68: cfg_type_changed = True jpayne@68: for item, value in page[section].items(): jpayne@68: if self.save_option(config_type, section, item, value): jpayne@68: cfg_type_changed = True jpayne@68: if cfg_type_changed: jpayne@68: idleConf.userCfg[config_type].Save() jpayne@68: changed = True jpayne@68: for config_type in ['keys', 'highlight']: jpayne@68: # Save these even if unchanged! jpayne@68: idleConf.userCfg[config_type].Save() jpayne@68: self.clear() jpayne@68: # ConfigDialog caller must add the following call jpayne@68: # self.save_all_changed_extensions() # Uses a different mechanism. jpayne@68: return changed jpayne@68: jpayne@68: def delete_section(self, config_type, section): jpayne@68: """Delete a section from self, userCfg, and file. jpayne@68: jpayne@68: Used to delete custom themes and keysets. jpayne@68: """ jpayne@68: if section in self[config_type]: jpayne@68: del self[config_type][section] jpayne@68: configpage = idleConf.userCfg[config_type] jpayne@68: configpage.remove_section(section) jpayne@68: configpage.Save() jpayne@68: jpayne@68: def clear(self): jpayne@68: """Clear all 4 pages. jpayne@68: jpayne@68: Called in save_all after saving to idleConf. jpayne@68: XXX Mark window *title* when there are changes; unmark here. jpayne@68: """ jpayne@68: for page in self.pages: jpayne@68: page.clear() jpayne@68: jpayne@68: jpayne@68: # TODO Revise test output, write expanded unittest jpayne@68: def _dump(): # htest # (not really, but ignore in coverage) jpayne@68: from zlib import crc32 jpayne@68: line, crc = 0, 0 jpayne@68: jpayne@68: def sprint(obj): jpayne@68: global line, crc jpayne@68: txt = str(obj) jpayne@68: line += 1 jpayne@68: crc = crc32(txt.encode(encoding='utf-8'), crc) jpayne@68: print(txt) jpayne@68: #print('***', line, crc, '***') # Uncomment for diagnosis. jpayne@68: jpayne@68: def dumpCfg(cfg): jpayne@68: print('\n', cfg, '\n') # Cfg has variable '0xnnnnnnnn' address. jpayne@68: for key in sorted(cfg.keys()): jpayne@68: sections = cfg[key].sections() jpayne@68: sprint(key) jpayne@68: sprint(sections) jpayne@68: for section in sections: jpayne@68: options = cfg[key].options(section) jpayne@68: sprint(section) jpayne@68: sprint(options) jpayne@68: for option in options: jpayne@68: sprint(option + ' = ' + cfg[key].Get(section, option)) jpayne@68: jpayne@68: dumpCfg(idleConf.defaultCfg) jpayne@68: dumpCfg(idleConf.userCfg) jpayne@68: print('\nlines = ', line, ', crc = ', crc, sep='') jpayne@68: jpayne@68: if __name__ == '__main__': jpayne@68: from unittest import main jpayne@68: main('idlelib.idle_test.test_config', verbosity=2, exit=False) jpayne@68: jpayne@68: # Run revised _dump() as htest?