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