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