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