Home | History | Annotate | Download | only in idlelib
      1 """Provides access to stored IDLE configuration information.
      2 
      3 Refer to the comments at the beginning of config-main.def for a description of
      4 the available configuration files and the design implemented to update user
      5 configuration information.  In particular, user configuration choices which
      6 duplicate the defaults will be removed from the user's configuration files,
      7 and if a file becomes empty, it will be deleted.
      8 
      9 The contents of the user files may be altered using the Options/Configure IDLE
     10 menu to access the configuration GUI (configDialog.py), or manually.
     11 
     12 Throughout this module there is an emphasis on returning useable defaults
     13 when a problem occurs in returning a requested configuration value back to
     14 idle. This is to allow IDLE to continue to function in spite of errors in
     15 the retrieval of config information. When a default is returned instead of
     16 a requested config value, a message is printed to stderr to aid in
     17 configuration problem notification and resolution.
     18 """
     19 # TODOs added Oct 2014, tjr
     20 
     21 from __future__ import print_function
     22 import os
     23 import sys
     24 
     25 from ConfigParser import ConfigParser
     26 from Tkinter import TkVersion
     27 from tkFont import Font, nametofont
     28 
     29 class InvalidConfigType(Exception): pass
     30 class InvalidConfigSet(Exception): pass
     31 class InvalidFgBg(Exception): pass
     32 class InvalidTheme(Exception): pass
     33 
     34 class IdleConfParser(ConfigParser):
     35     """
     36     A ConfigParser specialised for idle configuration file handling
     37     """
     38     def __init__(self, cfgFile, cfgDefaults=None):
     39         """
     40         cfgFile - string, fully specified configuration file name
     41         """
     42         self.file = cfgFile
     43         ConfigParser.__init__(self, defaults=cfgDefaults)
     44 
     45     def Get(self, section, option, type=None, default=None, raw=False):
     46         """
     47         Get an option value for given section/option or return default.
     48         If type is specified, return as type.
     49         """
     50         # TODO Use default as fallback, at least if not None
     51         # Should also print Warning(file, section, option).
     52         # Currently may raise ValueError
     53         if not self.has_option(section, option):
     54             return default
     55         if type == 'bool':
     56             return self.getboolean(section, option)
     57         elif type == 'int':
     58             return self.getint(section, option)
     59         else:
     60             return self.get(section, option, raw=raw)
     61 
     62     def GetOptionList(self, section):
     63         "Return a list of options for given section, else []."
     64         if self.has_section(section):
     65             return self.options(section)
     66         else:  #return a default value
     67             return []
     68 
     69     def Load(self):
     70         "Load the configuration file from disk."
     71         self.read(self.file)
     72 
     73 class IdleUserConfParser(IdleConfParser):
     74     """
     75     IdleConfigParser specialised for user configuration handling.
     76     """
     77 
     78     def AddSection(self, section):
     79         "If section doesn't exist, add it."
     80         if not self.has_section(section):
     81             self.add_section(section)
     82 
     83     def RemoveEmptySections(self):
     84         "Remove any sections that have no options."
     85         for section in self.sections():
     86             if not self.GetOptionList(section):
     87                 self.remove_section(section)
     88 
     89     def IsEmpty(self):
     90         "Return True if no sections after removing empty sections."
     91         self.RemoveEmptySections()
     92         return not self.sections()
     93 
     94     def RemoveOption(self, section, option):
     95         """Return True if option is removed from section, else False.
     96 
     97         False if either section does not exist or did not have option.
     98         """
     99         if self.has_section(section):
    100             return self.remove_option(section, option)
    101         return False
    102 
    103     def SetOption(self, section, option, value):
    104         """Return True if option is added or changed to value, else False.
    105 
    106         Add section if required.  False means option already had value.
    107         """
    108         if self.has_option(section, option):
    109             if self.get(section, option) == value:
    110                 return False
    111             else:
    112                 self.set(section, option, value)
    113                 return True
    114         else:
    115             if not self.has_section(section):
    116                 self.add_section(section)
    117             self.set(section, option, value)
    118             return True
    119 
    120     def RemoveFile(self):
    121         "Remove user config file self.file from disk if it exists."
    122         if os.path.exists(self.file):
    123             os.remove(self.file)
    124 
    125     def Save(self):
    126         """Update user configuration file.
    127 
    128         Remove empty sections. If resulting config isn't empty, write the file
    129         to disk. If config is empty, remove the file from disk if it exists.
    130 
    131         """
    132         if not self.IsEmpty():
    133             fname = self.file
    134             try:
    135                 cfgFile = open(fname, 'w')
    136             except IOError:
    137                 os.unlink(fname)
    138                 cfgFile = open(fname, 'w')
    139             with cfgFile:
    140                 self.write(cfgFile)
    141         else:
    142             self.RemoveFile()
    143 
    144 class IdleConf:
    145     """Hold config parsers for all idle config files in singleton instance.
    146 
    147     Default config files, self.defaultCfg --
    148         for config_type in self.config_types:
    149             (idle install dir)/config-{config-type}.def
    150 
    151     User config files, self.userCfg --
    152         for config_type in self.config_types:
    153         (user home dir)/.idlerc/config-{config-type}.cfg
    154     """
    155     def __init__(self):
    156         self.config_types = ('main', 'extensions', 'highlight', 'keys')
    157         self.defaultCfg = {}
    158         self.userCfg = {}
    159         self.cfg = {}  # TODO use to select userCfg vs defaultCfg
    160         self.CreateConfigHandlers()
    161         self.LoadCfgFiles()
    162 
    163 
    164     def CreateConfigHandlers(self):
    165         "Populate default and user config parser dictionaries."
    166         #build idle install path
    167         if __name__ != '__main__': # we were imported
    168             idleDir=os.path.dirname(__file__)
    169         else: # we were exec'ed (for testing only)
    170             idleDir=os.path.abspath(sys.path[0])
    171         userDir=self.GetUserCfgDir()
    172 
    173         defCfgFiles = {}
    174         usrCfgFiles = {}
    175         # TODO eliminate these temporaries by combining loops
    176         for cfgType in self.config_types: #build config file names
    177             defCfgFiles[cfgType] = os.path.join(
    178                     idleDir, 'config-' + cfgType + '.def')
    179             usrCfgFiles[cfgType] = os.path.join(
    180                     userDir, 'config-' + cfgType + '.cfg')
    181         for cfgType in self.config_types: #create config parsers
    182             self.defaultCfg[cfgType] = IdleConfParser(defCfgFiles[cfgType])
    183             self.userCfg[cfgType] = IdleUserConfParser(usrCfgFiles[cfgType])
    184 
    185     def GetUserCfgDir(self):
    186         """Return a filesystem directory for storing user config files.
    187 
    188         Creates it if required.
    189         """
    190         cfgDir = '.idlerc'
    191         userDir = os.path.expanduser('~')
    192         if userDir != '~': # expanduser() found user home dir
    193             if not os.path.exists(userDir):
    194                 warn = ('\n Warning: os.path.expanduser("~") points to\n ' +
    195                         userDir + ',\n but the path does not exist.')
    196                 try:
    197                     print(warn, file=sys.stderr)
    198                 except IOError:
    199                     pass
    200                 userDir = '~'
    201         if userDir == "~": # still no path to home!
    202             # traditionally IDLE has defaulted to os.getcwd(), is this adequate?
    203             userDir = os.getcwd()
    204         userDir = os.path.join(userDir, cfgDir)
    205         if not os.path.exists(userDir):
    206             try:
    207                 os.mkdir(userDir)
    208             except (OSError, IOError):
    209                 warn = ('\n Warning: unable to create user config directory\n' +
    210                         userDir + '\n Check path and permissions.\n Exiting!\n')
    211                 print(warn, file=sys.stderr)
    212                 raise SystemExit
    213         # TODO continue without userDIr instead of exit
    214         return userDir
    215 
    216     def GetOption(self, configType, section, option, default=None, type=None,
    217                   warn_on_default=True, raw=False):
    218         """Return a value for configType section option, or default.
    219 
    220         If type is not None, return a value of that type.  Also pass raw
    221         to the config parser.  First try to return a valid value
    222         (including type) from a user configuration. If that fails, try
    223         the default configuration. If that fails, return default, with a
    224         default of None.
    225 
    226         Warn if either user or default configurations have an invalid value.
    227         Warn if default is returned and warn_on_default is True.
    228         """
    229         try:
    230             if self.userCfg[configType].has_option(section, option):
    231                 return self.userCfg[configType].Get(section, option,
    232                                                     type=type, raw=raw)
    233         except ValueError:
    234             warning = ('\n Warning: configHandler.py - IdleConf.GetOption -\n'
    235                        ' invalid %r value for configuration option %r\n'
    236                        ' from section %r: %r' %
    237                        (type, option, section,
    238                        self.userCfg[configType].Get(section, option, raw=raw)))
    239             try:
    240                 print(warning, file=sys.stderr)
    241             except IOError:
    242                 pass
    243         try:
    244             if self.defaultCfg[configType].has_option(section,option):
    245                 return self.defaultCfg[configType].Get(
    246                         section, option, type=type, raw=raw)
    247         except ValueError:
    248             pass
    249         #returning default, print warning
    250         if warn_on_default:
    251             warning = ('\n Warning: configHandler.py - IdleConf.GetOption -\n'
    252                        ' problem retrieving configuration option %r\n'
    253                        ' from section %r.\n'
    254                        ' returning default value: %r' %
    255                        (option, section, default))
    256             try:
    257                 print(warning, file=sys.stderr)
    258             except IOError:
    259                 pass
    260         return default
    261 
    262     def SetOption(self, configType, section, option, value):
    263         """Set section option to value in user config file."""
    264         self.userCfg[configType].SetOption(section, option, value)
    265 
    266     def GetSectionList(self, configSet, configType):
    267         """Return sections for configSet configType configuration.
    268 
    269         configSet must be either 'user' or 'default'
    270         configType must be in self.config_types.
    271         """
    272         if not (configType in self.config_types):
    273             raise InvalidConfigType('Invalid configType specified')
    274         if configSet == 'user':
    275             cfgParser = self.userCfg[configType]
    276         elif configSet == 'default':
    277             cfgParser=self.defaultCfg[configType]
    278         else:
    279             raise InvalidConfigSet('Invalid configSet specified')
    280         return cfgParser.sections()
    281 
    282     def GetHighlight(self, theme, element, fgBg=None):
    283         """Return individual theme element highlight color(s).
    284 
    285         fgBg - string ('fg' or 'bg') or None.
    286         If None, return a dictionary containing fg and bg colors with
    287         keys 'foreground' and 'background'.  Otherwise, only return
    288         fg or bg color, as specified.  Colors are intended to be
    289         appropriate for passing to Tkinter in, e.g., a tag_config call).
    290         """
    291         if self.defaultCfg['highlight'].has_section(theme):
    292             themeDict = self.GetThemeDict('default', theme)
    293         else:
    294             themeDict = self.GetThemeDict('user', theme)
    295         fore = themeDict[element + '-foreground']
    296         if element == 'cursor':  # There is no config value for cursor bg
    297             back = themeDict['normal-background']
    298         else:
    299             back = themeDict[element + '-background']
    300         highlight = {"foreground": fore, "background": back}
    301         if not fgBg:  # Return dict of both colors
    302             return highlight
    303         else:  # Return specified color only
    304             if fgBg == 'fg':
    305                 return highlight["foreground"]
    306             if fgBg == 'bg':
    307                 return highlight["background"]
    308             else:
    309                 raise InvalidFgBg('Invalid fgBg specified')
    310 
    311     def GetThemeDict(self, type, themeName):
    312         """Return {option:value} dict for elements in themeName.
    313 
    314         type - string, 'default' or 'user' theme type
    315         themeName - string, theme name
    316         Values are loaded over ultimate fallback defaults to guarantee
    317         that all theme elements are present in a newly created theme.
    318         """
    319         if type == 'user':
    320             cfgParser = self.userCfg['highlight']
    321         elif type == 'default':
    322             cfgParser = self.defaultCfg['highlight']
    323         else:
    324             raise InvalidTheme('Invalid theme type specified')
    325         # Provide foreground and background colors for each theme
    326         # element (other than cursor) even though some values are not
    327         # yet used by idle, to allow for their use in the future.
    328         # Default values are generally black and white.
    329         # TODO copy theme from a class attribute.
    330         theme ={'normal-foreground':'#000000',
    331                 'normal-background':'#ffffff',
    332                 'keyword-foreground':'#000000',
    333                 'keyword-background':'#ffffff',
    334                 'builtin-foreground':'#000000',
    335                 'builtin-background':'#ffffff',
    336                 'comment-foreground':'#000000',
    337                 'comment-background':'#ffffff',
    338                 'string-foreground':'#000000',
    339                 'string-background':'#ffffff',
    340                 'definition-foreground':'#000000',
    341                 'definition-background':'#ffffff',
    342                 'hilite-foreground':'#000000',
    343                 'hilite-background':'gray',
    344                 'break-foreground':'#ffffff',
    345                 'break-background':'#000000',
    346                 'hit-foreground':'#ffffff',
    347                 'hit-background':'#000000',
    348                 'error-foreground':'#ffffff',
    349                 'error-background':'#000000',
    350                 #cursor (only foreground can be set)
    351                 'cursor-foreground':'#000000',
    352                 #shell window
    353                 'stdout-foreground':'#000000',
    354                 'stdout-background':'#ffffff',
    355                 'stderr-foreground':'#000000',
    356                 'stderr-background':'#ffffff',
    357                 'console-foreground':'#000000',
    358                 'console-background':'#ffffff' }
    359         for element in theme:
    360             if not cfgParser.has_option(themeName, element):
    361                 # Print warning that will return a default color
    362                 warning = ('\n Warning: configHandler.IdleConf.GetThemeDict'
    363                            ' -\n problem retrieving theme element %r'
    364                            '\n from theme %r.\n'
    365                            ' returning default color: %r' %
    366                            (element, themeName, theme[element]))
    367                 try:
    368                     print(warning, file=sys.stderr)
    369                 except IOError:
    370                     pass
    371             theme[element] = cfgParser.Get(
    372                     themeName, element, default=theme[element])
    373         return theme
    374 
    375     def CurrentTheme(self):
    376         """Return the name of the currently active text color theme.
    377 
    378         idlelib.config-main.def includes this section
    379         [Theme]
    380         default= 1
    381         name= IDLE Classic
    382         name2=
    383         # name2 set in user config-main.cfg for themes added after 2015 Oct 1
    384 
    385         Item name2 is needed because setting name to a new builtin
    386         causes older IDLEs to display multiple error messages or quit.
    387         See https://bugs.python.org/issue25313.
    388         When default = True, name2 takes precedence over name,
    389         while older IDLEs will just use name.
    390         """
    391         default = self.GetOption('main', 'Theme', 'default',
    392                                  type='bool', default=True)
    393         if default:
    394             theme = self.GetOption('main', 'Theme', 'name2', default='')
    395         if default and not theme or not default:
    396             theme = self.GetOption('main', 'Theme', 'name', default='')
    397         source = self.defaultCfg if default else self.userCfg
    398         if source['highlight'].has_section(theme):
    399             return theme
    400         else:
    401             return "IDLE Classic"
    402 
    403     def CurrentKeys(self):
    404         "Return the name of the currently active key set."
    405         return self.GetOption('main', 'Keys', 'name', default='')
    406 
    407     def GetExtensions(self, active_only=True, editor_only=False, shell_only=False):
    408         """Return extensions in default and user config-extensions files.
    409 
    410         If active_only True, only return active (enabled) extensions
    411         and optionally only editor or shell extensions.
    412         If active_only False, return all extensions.
    413         """
    414         extns = self.RemoveKeyBindNames(
    415                 self.GetSectionList('default', 'extensions'))
    416         userExtns = self.RemoveKeyBindNames(
    417                 self.GetSectionList('user', 'extensions'))
    418         for extn in userExtns:
    419             if extn not in extns: #user has added own extension
    420                 extns.append(extn)
    421         if active_only:
    422             activeExtns = []
    423             for extn in extns:
    424                 if self.GetOption('extensions', extn, 'enable', default=True,
    425                                   type='bool'):
    426                     #the extension is enabled
    427                     if editor_only or shell_only:  # TODO if both, contradictory
    428                         if editor_only:
    429                             option = "enable_editor"
    430                         else:
    431                             option = "enable_shell"
    432                         if self.GetOption('extensions', extn,option,
    433                                           default=True, type='bool',
    434                                           warn_on_default=False):
    435                             activeExtns.append(extn)
    436                     else:
    437                         activeExtns.append(extn)
    438             return activeExtns
    439         else:
    440             return extns
    441 
    442     def RemoveKeyBindNames(self, extnNameList):
    443         "Return extnNameList with keybinding section names removed."
    444         # TODO Easier to return filtered copy with list comp
    445         names = extnNameList
    446         kbNameIndicies = []
    447         for name in names:
    448             if name.endswith(('_bindings', '_cfgBindings')):
    449                 kbNameIndicies.append(names.index(name))
    450         kbNameIndicies.sort(reverse=True)
    451         for index in kbNameIndicies: #delete each keybinding section name
    452             del(names[index])
    453         return names
    454 
    455     def GetExtnNameForEvent(self, virtualEvent):
    456         """Return the name of the extension binding virtualEvent, or None.
    457 
    458         virtualEvent - string, name of the virtual event to test for,
    459                        without the enclosing '<< >>'
    460         """
    461         extName = None
    462         vEvent = '<<' + virtualEvent + '>>'
    463         for extn in self.GetExtensions(active_only=0):
    464             for event in self.GetExtensionKeys(extn):
    465                 if event == vEvent:
    466                     extName = extn  # TODO return here?
    467         return extName
    468 
    469     def GetExtensionKeys(self, extensionName):
    470         """Return dict: {configurable extensionName event : active keybinding}.
    471 
    472         Events come from default config extension_cfgBindings section.
    473         Keybindings come from GetCurrentKeySet() active key dict,
    474         where previously used bindings are disabled.
    475         """
    476         keysName = extensionName + '_cfgBindings'
    477         activeKeys = self.GetCurrentKeySet()
    478         extKeys = {}
    479         if self.defaultCfg['extensions'].has_section(keysName):
    480             eventNames = self.defaultCfg['extensions'].GetOptionList(keysName)
    481             for eventName in eventNames:
    482                 event = '<<' + eventName + '>>'
    483                 binding = activeKeys[event]
    484                 extKeys[event] = binding
    485         return extKeys
    486 
    487     def __GetRawExtensionKeys(self,extensionName):
    488         """Return dict {configurable extensionName event : keybinding list}.
    489 
    490         Events come from default config extension_cfgBindings section.
    491         Keybindings list come from the splitting of GetOption, which
    492         tries user config before default config.
    493         """
    494         keysName = extensionName+'_cfgBindings'
    495         extKeys = {}
    496         if self.defaultCfg['extensions'].has_section(keysName):
    497             eventNames = self.defaultCfg['extensions'].GetOptionList(keysName)
    498             for eventName in eventNames:
    499                 binding = self.GetOption(
    500                         'extensions', keysName, eventName, default='').split()
    501                 event = '<<' + eventName + '>>'
    502                 extKeys[event] = binding
    503         return extKeys
    504 
    505     def GetExtensionBindings(self, extensionName):
    506         """Return dict {extensionName event : active or defined keybinding}.
    507 
    508         Augment self.GetExtensionKeys(extensionName) with mapping of non-
    509         configurable events (from default config) to GetOption splits,
    510         as in self.__GetRawExtensionKeys.
    511         """
    512         bindsName = extensionName + '_bindings'
    513         extBinds = self.GetExtensionKeys(extensionName)
    514         #add the non-configurable bindings
    515         if self.defaultCfg['extensions'].has_section(bindsName):
    516             eventNames = self.defaultCfg['extensions'].GetOptionList(bindsName)
    517             for eventName in eventNames:
    518                 binding = self.GetOption(
    519                         'extensions', bindsName, eventName, default='').split()
    520                 event = '<<' + eventName + '>>'
    521                 extBinds[event] = binding
    522 
    523         return extBinds
    524 
    525     def GetKeyBinding(self, keySetName, eventStr):
    526         """Return the keybinding list for keySetName eventStr.
    527 
    528         keySetName - name of key binding set (config-keys section).
    529         eventStr - virtual event, including brackets, as in '<<event>>'.
    530         """
    531         eventName = eventStr[2:-2] #trim off the angle brackets
    532         binding = self.GetOption('keys', keySetName, eventName, default='').split()
    533         return binding
    534 
    535     def GetCurrentKeySet(self):
    536         "Return CurrentKeys with 'darwin' modifications."
    537         result = self.GetKeySet(self.CurrentKeys())
    538 
    539         if sys.platform == "darwin":
    540             # OS X Tk variants do not support the "Alt" keyboard modifier.
    541             # So replace all keybingings that use "Alt" with ones that
    542             # use the "Option" keyboard modifier.
    543             # TODO (Ned?): the "Option" modifier does not work properly for
    544             #        Cocoa Tk and XQuartz Tk so we should not use it
    545             #        in default OS X KeySets.
    546             for k, v in result.items():
    547                 v2 = [ x.replace('<Alt-', '<Option-') for x in v ]
    548                 if v != v2:
    549                     result[k] = v2
    550 
    551         return result
    552 
    553     def GetKeySet(self, keySetName):
    554         """Return event-key dict for keySetName core plus active extensions.
    555 
    556         If a binding defined in an extension is already in use, the
    557         extension binding is disabled by being set to ''
    558         """
    559         keySet = self.GetCoreKeys(keySetName)
    560         activeExtns = self.GetExtensions(active_only=1)
    561         for extn in activeExtns:
    562             extKeys = self.__GetRawExtensionKeys(extn)
    563             if extKeys: #the extension defines keybindings
    564                 for event in extKeys:
    565                     if extKeys[event] in keySet.values():
    566                         #the binding is already in use
    567                         extKeys[event] = '' #disable this binding
    568                     keySet[event] = extKeys[event] #add binding
    569         return keySet
    570 
    571     def IsCoreBinding(self, virtualEvent):
    572         """Return True if the virtual event is one of the core idle key events.
    573 
    574         virtualEvent - string, name of the virtual event to test for,
    575                        without the enclosing '<< >>'
    576         """
    577         return ('<<'+virtualEvent+'>>') in self.GetCoreKeys()
    578 
    579 # TODO make keyBindins a file or class attribute used for test above
    580 # and copied in function below
    581 
    582     def GetCoreKeys(self, keySetName=None):
    583         """Return dict of core virtual-key keybindings for keySetName.
    584 
    585         The default keySetName None corresponds to the keyBindings base
    586         dict. If keySetName is not None, bindings from the config
    587         file(s) are loaded _over_ these defaults, so if there is a
    588         problem getting any core binding there will be an 'ultimate last
    589         resort fallback' to the CUA-ish bindings defined here.
    590         """
    591         keyBindings={
    592             '<<copy>>': ['<Control-c>', '<Control-C>'],
    593             '<<cut>>': ['<Control-x>', '<Control-X>'],
    594             '<<paste>>': ['<Control-v>', '<Control-V>'],
    595             '<<beginning-of-line>>': ['<Control-a>', '<Home>'],
    596             '<<center-insert>>': ['<Control-l>'],
    597             '<<close-all-windows>>': ['<Control-q>'],
    598             '<<close-window>>': ['<Alt-F4>'],
    599             '<<do-nothing>>': ['<Control-x>'],
    600             '<<end-of-file>>': ['<Control-d>'],
    601             '<<python-docs>>': ['<F1>'],
    602             '<<python-context-help>>': ['<Shift-F1>'],
    603             '<<history-next>>': ['<Alt-n>'],
    604             '<<history-previous>>': ['<Alt-p>'],
    605             '<<interrupt-execution>>': ['<Control-c>'],
    606             '<<view-restart>>': ['<F6>'],
    607             '<<restart-shell>>': ['<Control-F6>'],
    608             '<<open-class-browser>>': ['<Alt-c>'],
    609             '<<open-module>>': ['<Alt-m>'],
    610             '<<open-new-window>>': ['<Control-n>'],
    611             '<<open-window-from-file>>': ['<Control-o>'],
    612             '<<plain-newline-and-indent>>': ['<Control-j>'],
    613             '<<print-window>>': ['<Control-p>'],
    614             '<<redo>>': ['<Control-y>'],
    615             '<<remove-selection>>': ['<Escape>'],
    616             '<<save-copy-of-window-as-file>>': ['<Alt-Shift-S>'],
    617             '<<save-window-as-file>>': ['<Alt-s>'],
    618             '<<save-window>>': ['<Control-s>'],
    619             '<<select-all>>': ['<Alt-a>'],
    620             '<<toggle-auto-coloring>>': ['<Control-slash>'],
    621             '<<undo>>': ['<Control-z>'],
    622             '<<find-again>>': ['<Control-g>', '<F3>'],
    623             '<<find-in-files>>': ['<Alt-F3>'],
    624             '<<find-selection>>': ['<Control-F3>'],
    625             '<<find>>': ['<Control-f>'],
    626             '<<replace>>': ['<Control-h>'],
    627             '<<goto-line>>': ['<Alt-g>'],
    628             '<<smart-backspace>>': ['<Key-BackSpace>'],
    629             '<<newline-and-indent>>': ['<Key-Return>', '<Key-KP_Enter>'],
    630             '<<smart-indent>>': ['<Key-Tab>'],
    631             '<<indent-region>>': ['<Control-Key-bracketright>'],
    632             '<<dedent-region>>': ['<Control-Key-bracketleft>'],
    633             '<<comment-region>>': ['<Alt-Key-3>'],
    634             '<<uncomment-region>>': ['<Alt-Key-4>'],
    635             '<<tabify-region>>': ['<Alt-Key-5>'],
    636             '<<untabify-region>>': ['<Alt-Key-6>'],
    637             '<<toggle-tabs>>': ['<Alt-Key-t>'],
    638             '<<change-indentwidth>>': ['<Alt-Key-u>'],
    639             '<<del-word-left>>': ['<Control-Key-BackSpace>'],
    640             '<<del-word-right>>': ['<Control-Key-Delete>']
    641             }
    642         if keySetName:
    643             for event in keyBindings:
    644                 binding = self.GetKeyBinding(keySetName, event)
    645                 if binding:
    646                     keyBindings[event] = binding
    647                 else: #we are going to return a default, print warning
    648                     warning=('\n Warning: configHandler.py - IdleConf.GetCoreKeys'
    649                                ' -\n problem retrieving key binding for event %r'
    650                                '\n from key set %r.\n'
    651                                ' returning default value: %r' %
    652                                (event, keySetName, keyBindings[event]))
    653                     try:
    654                         print(warning, file=sys.stderr)
    655                     except IOError:
    656                         pass
    657         return keyBindings
    658 
    659     def GetExtraHelpSourceList(self, configSet):
    660         """Return list of extra help sources from a given configSet.
    661 
    662         Valid configSets are 'user' or 'default'.  Return a list of tuples of
    663         the form (menu_item , path_to_help_file , option), or return the empty
    664         list.  'option' is the sequence number of the help resource.  'option'
    665         values determine the position of the menu items on the Help menu,
    666         therefore the returned list must be sorted by 'option'.
    667 
    668         """
    669         helpSources = []
    670         if configSet == 'user':
    671             cfgParser = self.userCfg['main']
    672         elif configSet == 'default':
    673             cfgParser = self.defaultCfg['main']
    674         else:
    675             raise InvalidConfigSet('Invalid configSet specified')
    676         options=cfgParser.GetOptionList('HelpFiles')
    677         for option in options:
    678             value=cfgParser.Get('HelpFiles', option, default=';')
    679             if value.find(';') == -1: #malformed config entry with no ';'
    680                 menuItem = '' #make these empty
    681                 helpPath = '' #so value won't be added to list
    682             else: #config entry contains ';' as expected
    683                 value=value.split(';')
    684                 menuItem=value[0].strip()
    685                 helpPath=value[1].strip()
    686             if menuItem and helpPath: #neither are empty strings
    687                 helpSources.append( (menuItem,helpPath,option) )
    688         helpSources.sort(key=lambda x: int(x[2]))
    689         return helpSources
    690 
    691     def GetAllExtraHelpSourcesList(self):
    692         """Return a list of the details of all additional help sources.
    693 
    694         Tuples in the list are those of GetExtraHelpSourceList.
    695         """
    696         allHelpSources = (self.GetExtraHelpSourceList('default') +
    697                 self.GetExtraHelpSourceList('user') )
    698         return allHelpSources
    699 
    700     def GetFont(self, root, configType, section):
    701         """Retrieve a font from configuration (font, font-size, font-bold)
    702         Intercept the special value 'TkFixedFont' and substitute
    703         the actual font, factoring in some tweaks if needed for
    704         appearance sakes.
    705 
    706         The 'root' parameter can normally be any valid Tkinter widget.
    707 
    708         Return a tuple (family, size, weight) suitable for passing
    709         to tkinter.Font
    710         """
    711         family = self.GetOption(configType, section, 'font', default='courier')
    712         size = self.GetOption(configType, section, 'font-size', type='int',
    713                               default='10')
    714         bold = self.GetOption(configType, section, 'font-bold', default=0,
    715                               type='bool')
    716         if (family == 'TkFixedFont'):
    717             if TkVersion < 8.5:
    718                 family = 'Courier'
    719             else:
    720                 f = Font(name='TkFixedFont', exists=True, root=root)
    721                 actualFont = Font.actual(f)
    722                 family = actualFont['family']
    723                 size = actualFont['size']
    724                 if size <= 0:
    725                     size = 10  # if font in pixels, ignore actual size
    726                 bold = actualFont['weight']=='bold'
    727         return (family, size, 'bold' if bold else 'normal')
    728 
    729     def LoadCfgFiles(self):
    730         "Load all configuration files."
    731         for key in self.defaultCfg:
    732             self.defaultCfg[key].Load()
    733             self.userCfg[key].Load() #same keys
    734 
    735     def SaveUserCfgFiles(self):
    736         "Write all loaded user configuration files to disk."
    737         for key in self.userCfg:
    738             self.userCfg[key].Save()
    739 
    740 
    741 idleConf = IdleConf()
    742 
    743 # TODO Revise test output, write expanded unittest
    744 #
    745 if __name__ == '__main__':
    746     from zlib import crc32
    747     line, crc = 0, 0
    748 
    749     def sprint(obj):
    750         global line, crc
    751         txt = str(obj)
    752         line += 1
    753         crc = crc32(txt.encode(encoding='utf-8'), crc)
    754         print(txt)
    755         #print('***', line, crc, '***')  # uncomment for diagnosis
    756 
    757     def dumpCfg(cfg):
    758         print('\n', cfg, '\n')  # has variable '0xnnnnnnnn' addresses
    759         for key in sorted(cfg.keys()):
    760             sections = cfg[key].sections()
    761             sprint(key)
    762             sprint(sections)
    763             for section in sections:
    764                 options = cfg[key].options(section)
    765                 sprint(section)
    766                 sprint(options)
    767                 for option in options:
    768                     sprint(option + ' = ' + cfg[key].Get(section, option))
    769 
    770     dumpCfg(idleConf.defaultCfg)
    771     dumpCfg(idleConf.userCfg)
    772     print('\nlines = ', line, ', crc = ', crc, sep='')
    773