annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/config.py @ 69:33d812a61356

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