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