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