1"""Provides access to stored IDLE configuration information.
2
3Refer to the comments at the beginning of config-main.def for a description of
4the available configuration files and the design implemented to update user
5configuration information.  In particular, user configuration choices which
6duplicate the defaults will be removed from the user's configuration files,
7and if a file becomes empty, it will be deleted.
8
9The contents of the user files may be altered using the Options/Configure IDLE
10menu to access the configuration GUI (configDialog.py), or manually.
11
12Throughout this module there is an emphasis on returning useable defaults
13when a problem occurs in returning a requested configuration value back to
14idle. This is to allow IDLE to continue to function in spite of errors in
15the retrieval of config information. When a default is returned instead of
16a requested config value, a message is printed to stderr to aid in
17configuration problem notification and resolution.
18"""
19# TODOs added Oct 2014, tjr
20
21from __future__ import print_function
22import os
23import sys
24
25from ConfigParser import ConfigParser
26from Tkinter import TkVersion
27from tkFont import Font, nametofont
28
29class InvalidConfigType(Exception): pass
30class InvalidConfigSet(Exception): pass
31class InvalidFgBg(Exception): pass
32class InvalidTheme(Exception): pass
33
34class 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
73class 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
144class 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
741idleConf = IdleConf()
742
743# TODO Revise test output, write expanded unittest
744#
745if __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