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