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