python (3.12.0)

(root)/
lib/
python3.12/
idlelib/
config.py
       1  """idlelib.config -- Manage IDLE configuration information.
       2  
       3  The comments at the beginning of config-main.def describe the
       4  configuration files and the design implemented to update user
       5  configuration information.  In particular, user configuration choices
       6  which duplicate the defaults will be removed from the user's
       7  configuration files, and if a user file becomes empty, it will be
       8  deleted.
       9  
      10  The configuration database maps options to values.  Conceptually, the
      11  database keys are tuples (config-type, section, item).  As implemented,
      12  there are  separate dicts for default and user values.  Each has
      13  config-type keys 'main', 'extensions', 'highlight', and 'keys'.  The
      14  value for each key is a ConfigParser instance that maps section and item
      15  to values.  For 'main' and 'extensions', user values override
      16  default values.  For 'highlight' and 'keys', user sections augment the
      17  default sections (and must, therefore, have distinct names).
      18  
      19  Throughout this module there is an emphasis on returning usable defaults
      20  when a problem occurs in returning a requested configuration value back to
      21  idle. This is to allow IDLE to continue to function in spite of errors in
      22  the retrieval of config information. When a default is returned instead of
      23  a requested config value, a message is printed to stderr to aid in
      24  configuration problem notification and resolution.
      25  """
      26  # TODOs added Oct 2014, tjr
      27  
      28  from configparser import ConfigParser
      29  import os
      30  import sys
      31  
      32  from tkinter.font import Font
      33  import idlelib
      34  
      35  class ESC[4;38;5;81mInvalidConfigType(ESC[4;38;5;149mException): pass
      36  class ESC[4;38;5;81mInvalidConfigSet(ESC[4;38;5;149mException): pass
      37  class ESC[4;38;5;81mInvalidTheme(ESC[4;38;5;149mException): pass
      38  
      39  class ESC[4;38;5;81mIdleConfParser(ESC[4;38;5;149mConfigParser):
      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  
      79  class ESC[4;38;5;81mIdleUserConfParser(ESC[4;38;5;149mIdleConfParser):
      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  
     145  class ESC[4;38;5;81mIdleConf:
     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 keyBindings 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  
     766  idleConf = IdleConf()
     767  
     768  _warned = set()
     769  def _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  
     779  class ESC[4;38;5;81mConfigChanges(ESC[4;38;5;149mdict):
     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
     878  def _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  
     907  if __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?