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