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