Coverage for config.py: 87%

407 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 13:22 -0700

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. 1aNTLYIngpflRhuemEvJAHOwXDGKqF

48 ConfigParser.__init__(self, defaults=cfgDefaults, strict=False) 1aNTLYIngpflRhuemEvJAHOwXDGKqF

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): 1aBsxortyNLngpflbdichuemvAwFQP

59 return default 1Nh

60 if type == 'bool': 1aBsxortyNLngpflbdichuemvAwFQP

61 return self.getboolean(section, option) 1asxortyNnflbdiceQ

62 elif type == 'int': 1aBsxortyNLngpflbdchuemvAwFP

63 return self.getint(section, option) 1aNmP

64 else: 

65 return self.get(section, option, raw=raw) 1aBsxortyNLngpflbdchuemvAwF

66 

67 def GetOptionList(self, section): 

68 "Return a list of options for given section, else []." 

69 if self.has_section(section): 1TLgbdceADGq

70 return self.options(section) 1TLgbdceADGq

71 else: #return a default value 

72 return [] 1T

73 

74 def Load(self): 

75 "Load the configuration file from disk." 

76 if self.file: 1aLYA

77 self.read(self.file) 1aLA

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): 1CjkBghwDGKqF

90 if self.get(section, option) == value: 1jBF

91 return False 1jBF

92 else: 

93 self.set(section, option, value) 1F

94 return True 1F

95 else: 

96 if not self.has_section(section): 1CjkBghwDGKqF

97 self.add_section(section) 1CjkBhwDGF

98 self.set(section, option, value) 1CjkBghwDGKqF

99 return True 1CjkBghwDGKqF

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): 1BK

107 return self.remove_option(section, option) 1BK

108 return False 1K

109 

110 def AddSection(self, section): 

111 "If section doesn't exist, add it." 

112 if not self.has_section(section): 1XDGKq

113 self.add_section(section) 1XDGKq

114 

115 def RemoveEmptySections(self): 

116 "Remove any sections that have no options." 

117 for section in self.sections(): 1DGq

118 if not self.GetOptionList(section): 1DGq

119 self.remove_section(section) 1DG

120 

121 def IsEmpty(self): 

122 "Return True if no sections after removing empty sections." 

123 self.RemoveEmptySections() 1Dq

124 return not self.sections() 1Dq

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 1Cjkq

133 if fname and fname[0] != '#': 1Cjkq

134 if not self.IsEmpty(): 1q

135 try: 1q

136 cfgFile = open(fname, 'w') 1q

137 except OSError: 

138 os.unlink(fname) 

139 cfgFile = open(fname, 'w') 

140 with cfgFile: 1q

141 self.write(cfgFile) 1q

142 elif os.path.exists(self.file): 142 ↛ exitline 142 didn't return from function 'Save', because the condition on line 142 was never false1q

143 os.remove(self.file) 1q

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') 1aInMgpflRhuemEvzJAHOw

158 self.defaultCfg = {} 1aInMgpflRhuemEvzJAHOw

159 self.userCfg = {} 1aInMgpflRhuemEvzJAHOw

160 self.cfg = {} # TODO use to select userCfg vs defaultCfg 1aInMgpflRhuemEvzJAHOw

161 # self.blink_off_time = <first editor text>['insertofftime'] 

162 # See https:/bugs.python.org/issue4630, msg356516. 

163 

164 if not _utest: 1aInMgpflRhuemEvzJAHOw

165 self.CreateConfigHandlers() 

166 self.LoadCfgFiles() 

167 

168 def CreateConfigHandlers(self): 

169 "Populate default and user config parser dictionaries." 

170 idledir = os.path.dirname(__file__) 1aI

171 self.userdir = userdir = '' if idlelib.testing else self.GetUserCfgDir() 1aI

172 for cfg_type in self.config_types: 1aI

173 self.defaultCfg[cfg_type] = IdleConfParser( 1aI

174 os.path.join(idledir, f'config-{cfg_type}.def')) 

175 self.userCfg[cfg_type] = IdleUserConfParser( 1aI

176 os.path.join(userdir or '#', f'config-{cfg_type}.cfg')) 

177 

178 def GetUserCfgDir(self): 

179 """Return a filesystem directory for storing user config files. 

180 

181 Creates it if required. 

182 """ 

183 cfgDir = '.idlerc' 1az

184 userDir = os.path.expanduser('~') 1az

185 if userDir != '~': # expanduser() found user home dir 1az

186 if not os.path.exists(userDir): 186 ↛ 187line 186 didn't jump to line 187, because the condition on line 186 was never true1az

187 if not idlelib.testing: 

188 warn = ('\n Warning: os.path.expanduser("~") points to\n ' + 

189 userDir + ',\n but the path does not exist.') 

190 try: 

191 print(warn, file=sys.stderr) 

192 except OSError: 

193 pass 

194 userDir = '~' 

195 if userDir == "~": # still no path to home! 1az

196 # traditionally IDLE has defaulted to os.getcwd(), is this adequate? 

197 userDir = os.getcwd() 1z

198 userDir = os.path.join(userDir, cfgDir) 1az

199 if not os.path.exists(userDir): 1az

200 try: 1z

201 os.mkdir(userDir) 1z

202 except OSError: 1z

203 if not idlelib.testing: 203 ↛ 204line 203 didn't jump to line 204, because the condition on line 203 was never true1z

204 warn = ('\n Warning: unable to create user config directory\n' + 

205 userDir + '\n Check path and permissions.\n Exiting!\n') 

206 try: 

207 print(warn, file=sys.stderr) 

208 except OSError: 

209 pass 

210 raise SystemExit 1z

211 # TODO continue without userDIr instead of exit 

212 return userDir 1az

213 

214 def GetOption(self, configType, section, option, default=None, type=None, 

215 warn_on_default=True, raw=False): 

216 """Return a value for configType section option, or default. 

217 

218 If type is not None, return a value of that type. Also pass raw 

219 to the config parser. First try to return a valid value 

220 (including type) from a user configuration. If that fails, try 

221 the default configuration. If that fails, return default, with a 

222 default of None. 

223 

224 Warn if either user or default configurations have an invalid value. 

225 Warn if default is returned and warn_on_default is True. 

226 """ 

227 try: 1asxortynpflbdicuemwQP

228 if self.userCfg[configType].has_option(section, option): 1asxortynpflbdicuemwQP

229 return self.userCfg[configType].Get(section, option, 1sxortynpflbdicuemw

230 type=type, raw=raw) 

231 except ValueError: 1m

232 warning = ('\n Warning: config.py - IdleConf.GetOption -\n' 1m

233 ' invalid %r value for configuration option %r\n' 

234 ' from section %r: %r' % 

235 (type, option, section, 

236 self.userCfg[configType].Get(section, option, raw=raw))) 

237 _warn(warning, configType, section, option) 1m

238 try: 1aorbdicuemQP

239 if self.defaultCfg[configType].has_option(section,option): 1aorbdicuemQP

240 return self.defaultCfg[configType].Get( 1aorbdicemQP

241 section, option, type=type, raw=raw) 

242 except ValueError: 1m

243 pass 1m

244 #returning default, print warning 

245 if warn_on_default: 1um

246 warning = ('\n Warning: config.py - IdleConf.GetOption -\n' 1m

247 ' problem retrieving configuration option %r\n' 

248 ' from section %r.\n' 

249 ' returning default value: %r' % 

250 (option, section, default)) 

251 _warn(warning, configType, section, option) 1m

252 return default 1um

253 

254 def SetOption(self, configType, section, option, value): 

255 """Set section option to value in user config file.""" 

256 self.userCfg[configType].SetOption(section, option, value) 1hw

257 

258 def GetSectionList(self, configSet, configType): 

259 """Return sections for configSet configType configuration. 

260 

261 configSet must be either 'user' or 'default' 

262 configType must be in self.config_types. 

263 """ 

264 if not (configType in self.config_types): 1afbdiceEH

265 raise InvalidConfigType('Invalid configType specified') 1E

266 if configSet == 'user': 1afbdiceEH

267 cfgParser = self.userCfg[configType] 1afbdiceE

268 elif configSet == 'default': 1afbdiceEH

269 cfgParser=self.defaultCfg[configType] 1afbdiceEH

270 else: 

271 raise InvalidConfigSet('Invalid configSet specified') 1E

272 return cfgParser.sections() 1afbdiceEH

273 

274 def GetHighlight(self, theme, element): 

275 """Return dict of theme element highlight colors. 

276 

277 The keys are 'foreground' and 'background'. The values are 

278 tkinter color strings for configuring backgrounds and tags. 

279 """ 

280 cfg = ('default' if self.defaultCfg['highlight'].has_section(theme) 1ah

281 else 'user') 

282 theme_dict = self.GetThemeDict(cfg, theme) 1ah

283 fore = theme_dict[element + '-foreground'] 1ah

284 if element == 'cursor': 1ah

285 element = 'normal' 1h

286 back = theme_dict[element + '-background'] 1ah

287 return {"foreground": fore, "background": back} 1ah

288 

289 def GetThemeDict(self, type, themeName): 

290 """Return {option:value} dict for elements in themeName. 

291 

292 type - string, 'default' or 'user' theme type 

293 themeName - string, theme name 

294 Values are loaded over ultimate fallback defaults to guarantee 

295 that all theme elements are present in a newly created theme. 

296 """ 

297 if type == 'user': 1ahv

298 cfgParser = self.userCfg['highlight'] 1hv

299 elif type == 'default': 1ahv

300 cfgParser = self.defaultCfg['highlight'] 1ahv

301 else: 

302 raise InvalidTheme('Invalid theme type specified') 1v

303 # Provide foreground and background colors for each theme 

304 # element (other than cursor) even though some values are not 

305 # yet used by idle, to allow for their use in the future. 

306 # Default values are generally black and white. 

307 # TODO copy theme from a class attribute. 

308 theme ={'normal-foreground':'#000000', 1ahv

309 'normal-background':'#ffffff', 

310 'keyword-foreground':'#000000', 

311 'keyword-background':'#ffffff', 

312 'builtin-foreground':'#000000', 

313 'builtin-background':'#ffffff', 

314 'comment-foreground':'#000000', 

315 'comment-background':'#ffffff', 

316 'string-foreground':'#000000', 

317 'string-background':'#ffffff', 

318 'definition-foreground':'#000000', 

319 'definition-background':'#ffffff', 

320 'hilite-foreground':'#000000', 

321 'hilite-background':'gray', 

322 'break-foreground':'#ffffff', 

323 'break-background':'#000000', 

324 'hit-foreground':'#ffffff', 

325 'hit-background':'#000000', 

326 'error-foreground':'#ffffff', 

327 'error-background':'#000000', 

328 'context-foreground':'#000000', 

329 'context-background':'#ffffff', 

330 'linenumber-foreground':'#000000', 

331 'linenumber-background':'#ffffff', 

332 #cursor (only foreground can be set) 

333 'cursor-foreground':'#000000', 

334 #shell window 

335 'stdout-foreground':'#000000', 

336 'stdout-background':'#ffffff', 

337 'stderr-foreground':'#000000', 

338 'stderr-background':'#ffffff', 

339 'console-foreground':'#000000', 

340 'console-background':'#ffffff', 

341 } 

342 for element in theme: 1ahv

343 if not (cfgParser.has_option(themeName, element) or 1ahv

344 # Skip warning for new elements. 

345 element.startswith(('context-', 'linenumber-'))): 

346 # Print warning that will return a default color 

347 warning = ('\n Warning: config.IdleConf.GetThemeDict' 1h

348 ' -\n problem retrieving theme element %r' 

349 '\n from theme %r.\n' 

350 ' returning default color: %r' % 

351 (element, themeName, theme[element])) 

352 _warn(warning, 'highlight', themeName, element) 1h

353 theme[element] = cfgParser.Get( 1ahv

354 themeName, element, default=theme[element]) 

355 return theme 1ahv

356 

357 def CurrentTheme(self): 

358 "Return the name of the currently active text color theme." 

359 return self.current_colors_and_keys('Theme') 1al

360 

361 def CurrentKeys(self): 

362 """Return the name of the currently active key set.""" 

363 return self.current_colors_and_keys('Keys') 1aflbdc

364 

365 def current_colors_and_keys(self, section): 

366 """Return the currently active name for Theme or Keys section. 

367 

368 idlelib.config-main.def ('default') includes these sections 

369 

370 [Theme] 

371 default= 1 

372 name= IDLE Classic 

373 name2= 

374 

375 [Keys] 

376 default= 1 

377 name= 

378 name2= 

379 

380 Item 'name2', is used for built-in ('default') themes and keys 

381 added after 2015 Oct 1 and 2016 July 1. This kludge is needed 

382 because setting 'name' to a builtin not defined in older IDLEs 

383 to display multiple error messages or quit. 

384 See https://bugs.python.org/issue25313. 

385 When default = True, 'name2' takes precedence over 'name', 

386 while older IDLEs will just use name. When default = False, 

387 'name2' may still be set, but it is ignored. 

388 """ 

389 cfgname = 'highlight' if section == 'Theme' else 'keys' 1asxortynflbdc

390 default = self.GetOption('main', section, 'default', 1asxortynflbdc

391 type='bool', default=True) 

392 name = '' 1asxortynflbdc

393 if default: 1asxortynflbdc

394 name = self.GetOption('main', section, 'name2', default='') 1asxornflbdc

395 if not name: 1asxortynflbdc

396 name = self.GetOption('main', section, 'name', default='') 1aortynflbdc

397 if name: 1asxortynflbdc

398 source = self.defaultCfg if default else self.userCfg 1asxortynl

399 if source[cfgname].has_section(name): 1asxortynl

400 return name 1asxortynl

401 return "IDLE Classic" if section == 'Theme' else self.default_keys() 1asxortyflbdc

402 

403 @staticmethod 

404 def default_keys(): 

405 if sys.platform[:3] == 'win': 1asotMflbdc

406 return 'IDLE Classic Windows' 1M

407 elif sys.platform == 'darwin': 1asotMflbdc

408 return 'IDLE Classic OSX' 1asotMlbdc

409 else: 

410 return 'IDLE Modern Unix' 1Mf

411 

412 def GetExtensions(self, active_only=True, 

413 editor_only=False, shell_only=False): 

414 """Return extensions in default and user config-extensions files. 

415 

416 If active_only True, only return active (enabled) extensions 

417 and optionally only editor or shell extensions. 

418 If active_only False, return all extensions. 

419 """ 

420 extns = self.RemoveKeyBindNames( 1afbdice

421 self.GetSectionList('default', 'extensions')) 

422 userExtns = self.RemoveKeyBindNames( 1afbdice

423 self.GetSectionList('user', 'extensions')) 

424 for extn in userExtns: 1afbdice

425 if extn not in extns: #user has added own extension 1fbdice

426 extns.append(extn) 1i

427 for extn in ('AutoComplete','CodeContext', 1afbdice

428 'FormatParagraph','ParenMatch'): 

429 extns.remove(extn) 1afbdice

430 # specific exclusions because we are storing config for mainlined old 

431 # extensions in config-extensions.def for backward compatibility 

432 if active_only: 1afbdice

433 activeExtns = [] 1afbdice

434 for extn in extns: 1afbdice

435 if self.GetOption('extensions', extn, 'enable', default=True, 1afbdice

436 type='bool'): 

437 #the extension is enabled 

438 if editor_only or shell_only: # TODO both True contradict 1bdice

439 if editor_only: 1i

440 option = "enable_editor" 1i

441 else: 

442 option = "enable_shell" 1i

443 if self.GetOption('extensions', extn,option, 1i

444 default=True, type='bool', 

445 warn_on_default=False): 

446 activeExtns.append(extn) 1i

447 else: 

448 activeExtns.append(extn) 1bdice

449 return activeExtns 1afbdice

450 else: 

451 return extns 1ic

452 

453 def RemoveKeyBindNames(self, extnNameList): 

454 "Return extnNameList with keybinding section names removed." 

455 return [n for n in extnNameList if not n.endswith(('_bindings', '_cfgBindings'))] 1afbdiceH

456 

457 def GetExtnNameForEvent(self, virtualEvent): 

458 """Return the name of the extension binding virtualEvent, or None. 

459 

460 virtualEvent - string, name of the virtual event to test for, 

461 without the enclosing '<< >>' 

462 """ 

463 extName = None 1c

464 vEvent = '<<' + virtualEvent + '>>' 1c

465 for extn in self.GetExtensions(active_only=0): 1c

466 for event in self.GetExtensionKeys(extn): 1c

467 if event == vEvent: 1c

468 extName = extn # TODO return here? 1c

469 return extName 1c

470 

471 def GetExtensionKeys(self, extensionName): 

472 """Return dict: {configurable extensionName event : active keybinding}. 

473 

474 Events come from default config extension_cfgBindings section. 

475 Keybindings come from GetCurrentKeySet() active key dict, 

476 where previously used bindings are disabled. 

477 """ 

478 keysName = extensionName + '_cfgBindings' 1bdc

479 activeKeys = self.GetCurrentKeySet() 1bdc

480 extKeys = {} 1bdc

481 if self.defaultCfg['extensions'].has_section(keysName): 1bdc

482 eventNames = self.defaultCfg['extensions'].GetOptionList(keysName) 1bdc

483 for eventName in eventNames: 1bdc

484 event = '<<' + eventName + '>>' 1bdc

485 binding = activeKeys[event] 1bdc

486 extKeys[event] = binding 1bdc

487 return extKeys 1bdc

488 

489 def __GetRawExtensionKeys(self,extensionName): 

490 """Return dict {configurable extensionName event : keybinding list}. 

491 

492 Events come from default config extension_cfgBindings section. 

493 Keybindings list come from the splitting of GetOption, which 

494 tries user config before default config. 

495 """ 

496 keysName = extensionName+'_cfgBindings' 1bdce

497 extKeys = {} 1bdce

498 if self.defaultCfg['extensions'].has_section(keysName): 498 ↛ 505line 498 didn't jump to line 505, because the condition on line 498 was never false1bdce

499 eventNames = self.defaultCfg['extensions'].GetOptionList(keysName) 1bdce

500 for eventName in eventNames: 1bdce

501 binding = self.GetOption( 1bdce

502 'extensions', keysName, eventName, default='').split() 

503 event = '<<' + eventName + '>>' 1bdce

504 extKeys[event] = binding 1bdce

505 return extKeys 1bdce

506 

507 def GetExtensionBindings(self, extensionName): 

508 """Return dict {extensionName event : active or defined keybinding}. 

509 

510 Augment self.GetExtensionKeys(extensionName) with mapping of non- 

511 configurable events (from default config) to GetOption splits, 

512 as in self.__GetRawExtensionKeys. 

513 """ 

514 bindsName = extensionName + '_bindings' 1b

515 extBinds = self.GetExtensionKeys(extensionName) 1b

516 #add the non-configurable bindings 

517 if self.defaultCfg['extensions'].has_section(bindsName): 1b

518 eventNames = self.defaultCfg['extensions'].GetOptionList(bindsName) 1b

519 for eventName in eventNames: 1b

520 binding = self.GetOption( 1b

521 'extensions', bindsName, eventName, default='').split() 

522 event = '<<' + eventName + '>>' 1b

523 extBinds[event] = binding 1b

524 

525 return extBinds 1b

526 

527 def GetKeyBinding(self, keySetName, eventStr): 

528 """Return the keybinding list for keySetName eventStr. 

529 

530 keySetName - name of key binding set (config-keys section). 

531 eventStr - virtual event, including brackets, as in '<<event>>'. 

532 """ 

533 eventName = eventStr[2:-2] #trim off the angle brackets 1apfbdcue

534 binding = self.GetOption('keys', keySetName, eventName, default='', 1apfbdcue

535 warn_on_default=False).split() 

536 return binding 1apfbdcue

537 

538 def GetCurrentKeySet(self): 

539 "Return CurrentKeys with 'darwin' modifications." 

540 result = self.GetKeySet(self.CurrentKeys()) 1afbdc

541 

542 if sys.platform == "darwin": 1afbdc

543 # macOS (OS X) Tk variants do not support the "Alt" 

544 # keyboard modifier. Replace it with "Option". 

545 # TODO (Ned?): the "Option" modifier does not work properly 

546 # for Cocoa Tk and XQuartz Tk so we should not use it 

547 # in the default 'OSX' keyset. 

548 for k, v in result.items(): 1abdc

549 v2 = [ x.replace('<Alt-', '<Option-') for x in v ] 1abdc

550 if v != v2: 550 ↛ 551line 550 didn't jump to line 551, because the condition on line 550 was never true1abdc

551 result[k] = v2 

552 

553 return result 1afbdc

554 

555 def GetKeySet(self, keySetName): 

556 """Return event-key dict for keySetName core plus active extensions. 

557 

558 If a binding defined in an extension is already in use, the 

559 extension binding is disabled by being set to '' 

560 """ 

561 keySet = self.GetCoreKeys(keySetName) 1afbdce

562 activeExtns = self.GetExtensions(active_only=1) 1afbdce

563 for extn in activeExtns: 1afbdce

564 extKeys = self.__GetRawExtensionKeys(extn) 1bdce

565 if extKeys: #the extension defines keybindings 565 ↛ 563line 565 didn't jump to line 563, because the condition on line 565 was never false1bdce

566 for event in extKeys: 1bdce

567 if extKeys[event] in keySet.values(): 1bdce

568 #the binding is already in use 

569 extKeys[event] = '' #disable this binding 1e

570 keySet[event] = extKeys[event] #add binding 1bdce

571 return keySet 1afbdce

572 

573 def IsCoreBinding(self, virtualEvent): 

574 """Return True if the virtual event is one of the core idle key events. 

575 

576 virtualEvent - string, name of the virtual event to test for, 

577 without the enclosing '<< >>' 

578 """ 

579 return ('<<'+virtualEvent+'>>') in self.GetCoreKeys() 1J

580 

581# TODO make keyBindings a file or class attribute used for test above 

582# and copied in function below. 

583 

584 former_extension_events = { # Those with user-configurable keys. 

585 '<<force-open-completions>>', '<<expand-word>>', 

586 '<<force-open-calltip>>', '<<flash-paren>>', '<<format-paragraph>>', 

587 '<<run-module>>', '<<check-module>>', '<<zoom-height>>', 

588 '<<run-custom>>', 

589 } 

590 

591 def GetCoreKeys(self, keySetName=None): 

592 """Return dict of core virtual-key keybindings for keySetName. 

593 

594 The default keySetName None corresponds to the keyBindings base 

595 dict. If keySetName is not None, bindings from the config 

596 file(s) are loaded _over_ these defaults, so if there is a 

597 problem getting any core binding there will be an 'ultimate last 

598 resort fallback' to the CUA-ish bindings defined here. 

599 """ 

600 keyBindings={ 1apfbdceJ

601 '<<copy>>': ['<Control-c>', '<Control-C>'], 

602 '<<cut>>': ['<Control-x>', '<Control-X>'], 

603 '<<paste>>': ['<Control-v>', '<Control-V>'], 

604 '<<beginning-of-line>>': ['<Control-a>', '<Home>'], 

605 '<<center-insert>>': ['<Control-l>'], 

606 '<<close-all-windows>>': ['<Control-q>'], 

607 '<<close-window>>': ['<Alt-F4>'], 

608 '<<do-nothing>>': ['<Control-x>'], 

609 '<<end-of-file>>': ['<Control-d>'], 

610 '<<python-docs>>': ['<F1>'], 

611 '<<python-context-help>>': ['<Shift-F1>'], 

612 '<<history-next>>': ['<Alt-n>'], 

613 '<<history-previous>>': ['<Alt-p>'], 

614 '<<interrupt-execution>>': ['<Control-c>'], 

615 '<<view-restart>>': ['<F6>'], 

616 '<<restart-shell>>': ['<Control-F6>'], 

617 '<<open-class-browser>>': ['<Alt-c>'], 

618 '<<open-module>>': ['<Alt-m>'], 

619 '<<open-new-window>>': ['<Control-n>'], 

620 '<<open-window-from-file>>': ['<Control-o>'], 

621 '<<plain-newline-and-indent>>': ['<Control-j>'], 

622 '<<print-window>>': ['<Control-p>'], 

623 '<<redo>>': ['<Control-y>'], 

624 '<<remove-selection>>': ['<Escape>'], 

625 '<<save-copy-of-window-as-file>>': ['<Alt-Shift-S>'], 

626 '<<save-window-as-file>>': ['<Alt-s>'], 

627 '<<save-window>>': ['<Control-s>'], 

628 '<<select-all>>': ['<Alt-a>'], 

629 '<<toggle-auto-coloring>>': ['<Control-slash>'], 

630 '<<undo>>': ['<Control-z>'], 

631 '<<find-again>>': ['<Control-g>', '<F3>'], 

632 '<<find-in-files>>': ['<Alt-F3>'], 

633 '<<find-selection>>': ['<Control-F3>'], 

634 '<<find>>': ['<Control-f>'], 

635 '<<replace>>': ['<Control-h>'], 

636 '<<goto-line>>': ['<Alt-g>'], 

637 '<<smart-backspace>>': ['<Key-BackSpace>'], 

638 '<<newline-and-indent>>': ['<Key-Return>', '<Key-KP_Enter>'], 

639 '<<smart-indent>>': ['<Key-Tab>'], 

640 '<<indent-region>>': ['<Control-Key-bracketright>'], 

641 '<<dedent-region>>': ['<Control-Key-bracketleft>'], 

642 '<<comment-region>>': ['<Alt-Key-3>'], 

643 '<<uncomment-region>>': ['<Alt-Key-4>'], 

644 '<<tabify-region>>': ['<Alt-Key-5>'], 

645 '<<untabify-region>>': ['<Alt-Key-6>'], 

646 '<<toggle-tabs>>': ['<Alt-Key-t>'], 

647 '<<change-indentwidth>>': ['<Alt-Key-u>'], 

648 '<<del-word-left>>': ['<Control-Key-BackSpace>'], 

649 '<<del-word-right>>': ['<Control-Key-Delete>'], 

650 '<<force-open-completions>>': ['<Control-Key-space>'], 

651 '<<expand-word>>': ['<Alt-Key-slash>'], 

652 '<<force-open-calltip>>': ['<Control-Key-backslash>'], 

653 '<<flash-paren>>': ['<Control-Key-0>'], 

654 '<<format-paragraph>>': ['<Alt-Key-q>'], 

655 '<<run-module>>': ['<Key-F5>'], 

656 '<<run-custom>>': ['<Shift-Key-F5>'], 

657 '<<check-module>>': ['<Alt-Key-x>'], 

658 '<<zoom-height>>': ['<Alt-Key-2>'], 

659 } 

660 

661 if keySetName: 1apfbdceJ

662 if not (self.userCfg['keys'].has_section(keySetName) or 662 ↛ 664line 662 didn't jump to line 6641apfbdce

663 self.defaultCfg['keys'].has_section(keySetName)): 

664 warning = ( 

665 '\n Warning: config.py - IdleConf.GetCoreKeys -\n' 

666 ' key set %r is not defined, using default bindings.' % 

667 (keySetName,) 

668 ) 

669 _warn(warning, 'keys', keySetName) 

670 else: 

671 for event in keyBindings: 1apfbdce

672 binding = self.GetKeyBinding(keySetName, event) 1apfbdce

673 if binding: 673 ↛ 676line 673 didn't jump to line 676, because the condition on line 673 was never false1apfbdce

674 keyBindings[event] = binding 1apfbdce

675 # Otherwise return default in keyBindings. 

676 elif event not in self.former_extension_events: 

677 warning = ( 

678 '\n Warning: config.py - IdleConf.GetCoreKeys -\n' 

679 ' problem retrieving key binding for event %r\n' 

680 ' from key set %r.\n' 

681 ' returning default value: %r' % 

682 (event, keySetName, keyBindings[event]) 

683 ) 

684 _warn(warning, 'keys', keySetName, event) 

685 return keyBindings 1apfbdceJ

686 

687 def GetExtraHelpSourceList(self, configSet): 

688 """Return list of extra help sources from a given configSet. 

689 

690 Valid configSets are 'user' or 'default'. Return a list of tuples of 

691 the form (menu_item , path_to_help_file , option), or return the empty 

692 list. 'option' is the sequence number of the help resource. 'option' 

693 values determine the position of the menu items on the Help menu, 

694 therefore the returned list must be sorted by 'option'. 

695 

696 """ 

697 helpSources = [] 1g

698 if configSet == 'user': 1g

699 cfgParser = self.userCfg['main'] 1g

700 elif configSet == 'default': 1g

701 cfgParser = self.defaultCfg['main'] 1g

702 else: 

703 raise InvalidConfigSet('Invalid configSet specified') 1g

704 options=cfgParser.GetOptionList('HelpFiles') 1g

705 for option in options: 1g

706 value=cfgParser.Get('HelpFiles', option, default=';') 1g

707 if value.find(';') == -1: #malformed config entry with no ';' 1g

708 menuItem = '' #make these empty 1g

709 helpPath = '' #so value won't be added to list 1g

710 else: #config entry contains ';' as expected 

711 value=value.split(';') 1g

712 menuItem=value[0].strip() 1g

713 helpPath=value[1].strip() 1g

714 if menuItem and helpPath: #neither are empty strings 1g

715 helpSources.append( (menuItem,helpPath,option) ) 1g

716 helpSources.sort(key=lambda x: x[2]) 1g

717 return helpSources 1g

718 

719 def GetAllExtraHelpSourcesList(self): 

720 """Return a list of the details of all additional help sources. 

721 

722 Tuples in the list are those of GetExtraHelpSourceList. 

723 """ 

724 allHelpSources = (self.GetExtraHelpSourceList('default') + 1g

725 self.GetExtraHelpSourceList('user') ) 

726 return allHelpSources 1g

727 

728 def GetFont(self, root, configType, section): 

729 """Retrieve a font from configuration (font, font-size, font-bold) 

730 Intercept the special value 'TkFixedFont' and substitute 

731 the actual font, factoring in some tweaks if needed for 

732 appearance sakes. 

733 

734 The 'root' parameter can normally be any valid Tkinter widget. 

735 

736 Return a tuple (family, size, weight) suitable for passing 

737 to tkinter.Font 

738 """ 

739 family = self.GetOption(configType, section, 'font', default='courier') 

740 size = self.GetOption(configType, section, 'font-size', type='int', 

741 default='10') 

742 bold = self.GetOption(configType, section, 'font-bold', default=0, 

743 type='bool') 

744 if (family == 'TkFixedFont'): 

745 f = Font(name='TkFixedFont', exists=True, root=root) 

746 actualFont = Font.actual(f) 

747 family = actualFont['family'] 

748 size = actualFont['size'] 

749 if size <= 0: 

750 size = 10 # if font in pixels, ignore actual size 

751 bold = actualFont['weight'] == 'bold' 

752 return (family, size, 'bold' if bold else 'normal') 

753 

754 def LoadCfgFiles(self): 

755 "Load all configuration files." 

756 for key in self.defaultCfg: 1aA

757 self.defaultCfg[key].Load() 1aA

758 self.userCfg[key].Load() #same keys 1aA

759 

760 def SaveUserCfgFiles(self): 

761 "Write all loaded user configuration files to disk." 

762 for key in self.userCfg: 1O

763 self.userCfg[key].Save() 1O

764 

765 

766idleConf = IdleConf() 

767 

768_warned = set() 

769def _warn(msg, *key): 

770 key = (msg,) + key 1U

771 if key not in _warned: 1U

772 try: 1U

773 print(msg, file=sys.stderr) 1U

774 except OSError: 

775 pass 

776 _warned.add(key) 1U

777 

778 

779class ConfigChanges(dict): 

780 """Manage a user's proposed configuration option changes. 

781 

782 Names used across multiple methods: 

783 page -- one of the 4 top-level dicts representing a 

784 .idlerc/config-x.cfg file. 

785 config_type -- name of a page. 

786 section -- a section within a page/file. 

787 option -- name of an option within a section. 

788 value -- value for the option. 

789 

790 Methods 

791 add_option: Add option and value to changes. 

792 save_option: Save option and value to config parser. 

793 save_all: Save all the changes to the config parser and file. 

794 delete_section: If section exists, 

795 delete from changes, userCfg, and file. 

796 clear: Clear all changes by clearing each page. 

797 """ 

798 def __init__(self): 

799 "Create a page for each configuration file" 

800 self.pages = [] # List of unhashable dicts. 

801 for config_type in idleConf.config_types: 

802 self[config_type] = {} 

803 self.pages.append(self[config_type]) 

804 

805 def add_option(self, config_type, section, item, value): 

806 "Add item/value pair for config_type and section." 

807 page = self[config_type] 1VSCjWk

808 value = str(value) # Make sure we use a string. 1VSCjWk

809 if section not in page: 1VSCjWk

810 page[section] = {} 1VSCjWk

811 page[section][item] = value 1VSCjWk

812 

813 @staticmethod 

814 def save_option(config_type, section, item, value): 

815 """Return True if the configuration value was added or changed. 

816 

817 Helper for save_all. 

818 """ 

819 if idleConf.defaultCfg[config_type].has_option(section, item): 1jkB

820 if idleConf.defaultCfg[config_type].Get(section, item) == value: 1B

821 # The setting equals a default setting, remove it from user cfg. 

822 return idleConf.userCfg[config_type].RemoveOption(section, item) 1B

823 # If we got here, set the option. 

824 return idleConf.userCfg[config_type].SetOption(section, item, value) 1jkB

825 

826 def save_all(self): 

827 """Save configuration changes to the user config file. 

828 

829 Clear self in preparation for additional changes. 

830 Return changed for testing. 

831 """ 

832 idleConf.userCfg['main'].Save() 1jk

833 

834 changed = False 1jk

835 for config_type in self: 1jk

836 cfg_type_changed = False 1jk

837 page = self[config_type] 1jk

838 for section in page: 1jk

839 if section == 'HelpFiles': # Remove it for replacement. 1jk

840 idleConf.userCfg['main'].remove_section('HelpFiles') 1k

841 cfg_type_changed = True 1k

842 for item, value in page[section].items(): 1jk

843 if self.save_option(config_type, section, item, value): 1jk

844 cfg_type_changed = True 1jk

845 if cfg_type_changed: 1jk

846 idleConf.userCfg[config_type].Save() 1jk

847 changed = True 1jk

848 for config_type in ['keys', 'highlight']: 1jk

849 # Save these even if unchanged! 

850 idleConf.userCfg[config_type].Save() 1jk

851 self.clear() 1jk

852 # ConfigDialog caller must add the following call 

853 # self.save_all_changed_extensions() # Uses a different mechanism. 

854 return changed 1jk

855 

856 def delete_section(self, config_type, section): 

857 """Delete a section from self, userCfg, and file. 

858 

859 Used to delete custom themes and keysets. 

860 """ 

861 if section in self[config_type]: 1C

862 del self[config_type][section] 1C

863 configpage = idleConf.userCfg[config_type] 1C

864 configpage.remove_section(section) 1C

865 configpage.Save() 1C

866 

867 def clear(self): 

868 """Clear all 4 pages. 

869 

870 Called in save_all after saving to idleConf. 

871 XXX Mark window *title* when there are changes; unmark here. 

872 """ 

873 for page in self.pages: 1Sjk

874 page.clear() 1Sjk

875 

876 

877# TODO Revise test output, write expanded unittest 

878def _dump(): # htest # (not really, but ignore in coverage) 

879 from zlib import crc32 

880 line, crc = 0, 0 

881 

882 def sprint(obj): 

883 global line, crc 

884 txt = str(obj) 

885 line += 1 

886 crc = crc32(txt.encode(encoding='utf-8'), crc) 

887 print(txt) 

888 #print('***', line, crc, '***') # Uncomment for diagnosis. 

889 

890 def dumpCfg(cfg): 

891 print('\n', cfg, '\n') # Cfg has variable '0xnnnnnnnn' address. 

892 for key in sorted(cfg.keys()): 

893 sections = cfg[key].sections() 

894 sprint(key) 

895 sprint(sections) 

896 for section in sections: 

897 options = cfg[key].options(section) 

898 sprint(section) 

899 sprint(options) 

900 for option in options: 

901 sprint(option + ' = ' + cfg[key].Get(section, option)) 

902 

903 dumpCfg(idleConf.defaultCfg) 

904 dumpCfg(idleConf.userCfg) 

905 print('\nlines = ', line, ', crc = ', crc, sep='') 

906 

907if __name__ == '__main__': 907 ↛ 908line 907 didn't jump to line 908, because the condition on line 907 was never true

908 from unittest import main 

909 main('idlelib.idle_test.test_config', verbosity=2, exit=False) 

910 

911 # Run revised _dump() as htest?