annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/config.py @ 68:5028fdace37b

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