from configparser import ConfigParser
import os
import sys
from tkinter.font import Font
import idlelib
class InvalidConfigType(Exception): pass
class InvalidConfigSet(Exception): pass
class InvalidTheme(Exception): pass
class IdleConfParser(ConfigParser):
def __init__(self, cfgFile, cfgDefaults=None):
self.file = cfgFile ConfigParser.__init__(self, defaults=cfgDefaults, strict=False)
def Get(self, section, option, type=None, default=None, raw=False):
if not self.has_option(section, option):
return default
if type == 'bool':
return self.getboolean(section, option)
elif type == 'int':
return self.getint(section, option)
else:
return self.get(section, option, raw=raw)
def GetOptionList(self, section):
"Return a list of options for given section, else []."
if self.has_section(section):
return self.options(section)
else: return []
def Load(self):
"Load the configuration file from disk."
if self.file:
self.read(self.file)
class IdleUserConfParser(IdleConfParser):
def SetOption(self, section, option, value):
if self.has_option(section, option):
if self.get(section, option) == value:
return False
else:
self.set(section, option, value)
return True
else:
if not self.has_section(section):
self.add_section(section)
self.set(section, option, value)
return True
def RemoveOption(self, section, option):
if self.has_section(section):
return self.remove_option(section, option)
return False
def AddSection(self, section):
"If section doesn't exist, add it."
if not self.has_section(section):
self.add_section(section)
def RemoveEmptySections(self):
"Remove any sections that have no options."
for section in self.sections():
if not self.GetOptionList(section):
self.remove_section(section)
def IsEmpty(self):
"Return True if no sections after removing empty sections."
self.RemoveEmptySections()
return not self.sections()
def Save(self):
fname = self.file
if fname and fname[0] != '#':
if not self.IsEmpty():
try:
cfgFile = open(fname, 'w')
except OSError:
os.unlink(fname)
cfgFile = open(fname, 'w')
with cfgFile:
self.write(cfgFile)
elif os.path.exists(self.file):
os.remove(self.file)
class IdleConf:
def __init__(self, _utest=False):
self.config_types = ('main', 'highlight', 'keys', 'extensions')
self.defaultCfg = {}
self.userCfg = {}
self.cfg = {}
if not _utest:
self.CreateConfigHandlers()
self.LoadCfgFiles()
def CreateConfigHandlers(self):
"Populate default and user config parser dictionaries."
idledir = os.path.dirname(__file__)
self.userdir = userdir = '' if idlelib.testing else self.GetUserCfgDir()
for cfg_type in self.config_types:
self.defaultCfg[cfg_type] = IdleConfParser(
os.path.join(idledir, f'config-{cfg_type}.def'))
self.userCfg[cfg_type] = IdleUserConfParser(
os.path.join(userdir or '#', f'config-{cfg_type}.cfg'))
def GetUserCfgDir(self):
cfgDir = '.idlerc'
userDir = os.path.expanduser('~')
if userDir != '~': if not os.path.exists(userDir):
if not idlelib.testing:
warn = ('\n Warning: os.path.expanduser("~") points to\n ' +
userDir + ',\n but the path does not exist.')
try:
print(warn, file=sys.stderr)
except OSError:
pass
userDir = '~'
if userDir == "~": userDir = os.getcwd()
userDir = os.path.join(userDir, cfgDir)
if not os.path.exists(userDir):
try:
os.mkdir(userDir)
except OSError:
if not idlelib.testing:
warn = ('\n Warning: unable to create user config directory\n' +
userDir + '\n Check path and permissions.\n Exiting!\n')
try:
print(warn, file=sys.stderr)
except OSError:
pass
raise SystemExit
return userDir
def GetOption(self, configType, section, option, default=None, type=None,
warn_on_default=True, raw=False):
try:
if self.userCfg[configType].has_option(section, option):
return self.userCfg[configType].Get(section, option,
type=type, raw=raw)
except ValueError:
warning = ('\n Warning: config.py - IdleConf.GetOption -\n'
' invalid %r value for configuration option %r\n'
' from section %r: %r' %
(type, option, section,
self.userCfg[configType].Get(section, option, raw=raw)))
_warn(warning, configType, section, option)
try:
if self.defaultCfg[configType].has_option(section,option):
return self.defaultCfg[configType].Get(
section, option, type=type, raw=raw)
except ValueError:
pass
if warn_on_default:
warning = ('\n Warning: config.py - IdleConf.GetOption -\n'
' problem retrieving configuration option %r\n'
' from section %r.\n'
' returning default value: %r' %
(option, section, default))
_warn(warning, configType, section, option)
return default
def SetOption(self, configType, section, option, value):
self.userCfg[configType].SetOption(section, option, value)
def GetSectionList(self, configSet, configType):
if not (configType in self.config_types):
raise InvalidConfigType('Invalid configType specified')
if configSet == 'user':
cfgParser = self.userCfg[configType]
elif configSet == 'default':
cfgParser=self.defaultCfg[configType]
else:
raise InvalidConfigSet('Invalid configSet specified')
return cfgParser.sections()
def GetHighlight(self, theme, element):
cfg = ('default' if self.defaultCfg['highlight'].has_section(theme)
else 'user')
theme_dict = self.GetThemeDict(cfg, theme)
fore = theme_dict[element + '-foreground']
if element == 'cursor':
element = 'normal'
back = theme_dict[element + '-background']
return {"foreground": fore, "background": back}
def GetThemeDict(self, type, themeName):
if type == 'user':
cfgParser = self.userCfg['highlight']
elif type == 'default':
cfgParser = self.defaultCfg['highlight']
else:
raise InvalidTheme('Invalid theme type specified')
theme ={'normal-foreground':'#000000',
'normal-background':'#ffffff',
'keyword-foreground':'#000000',
'keyword-background':'#ffffff',
'builtin-foreground':'#000000',
'builtin-background':'#ffffff',
'comment-foreground':'#000000',
'comment-background':'#ffffff',
'string-foreground':'#000000',
'string-background':'#ffffff',
'definition-foreground':'#000000',
'definition-background':'#ffffff',
'hilite-foreground':'#000000',
'hilite-background':'gray',
'break-foreground':'#ffffff',
'break-background':'#000000',
'hit-foreground':'#ffffff',
'hit-background':'#000000',
'error-foreground':'#ffffff',
'error-background':'#000000',
'context-foreground':'#000000',
'context-background':'#ffffff',
'linenumber-foreground':'#000000',
'linenumber-background':'#ffffff',
'cursor-foreground':'#000000',
'stdout-foreground':'#000000',
'stdout-background':'#ffffff',
'stderr-foreground':'#000000',
'stderr-background':'#ffffff',
'console-foreground':'#000000',
'console-background':'#ffffff',
}
for element in theme:
if not (cfgParser.has_option(themeName, element) or
element.startswith(('context-', 'linenumber-'))):
warning = ('\n Warning: config.IdleConf.GetThemeDict'
' -\n problem retrieving theme element %r'
'\n from theme %r.\n'
' returning default color: %r' %
(element, themeName, theme[element]))
_warn(warning, 'highlight', themeName, element)
theme[element] = cfgParser.Get(
themeName, element, default=theme[element])
return theme
def CurrentTheme(self):
"Return the name of the currently active text color theme."
return self.current_colors_and_keys('Theme')
def CurrentKeys(self):
return self.current_colors_and_keys('Keys')
def current_colors_and_keys(self, section):
cfgname = 'highlight' if section == 'Theme' else 'keys'
default = self.GetOption('main', section, 'default',
type='bool', default=True)
name = ''
if default:
name = self.GetOption('main', section, 'name2', default='')
if not name:
name = self.GetOption('main', section, 'name', default='')
if name:
source = self.defaultCfg if default else self.userCfg
if source[cfgname].has_section(name):
return name
return "IDLE Classic" if section == 'Theme' else self.default_keys()
@staticmethod
def default_keys():
if sys.platform[:3] == 'win':
return 'IDLE Classic Windows'
elif sys.platform == 'darwin':
return 'IDLE Classic OSX'
else:
return 'IDLE Modern Unix'
def GetExtensions(self, active_only=True,
editor_only=False, shell_only=False):
extns = self.RemoveKeyBindNames(
self.GetSectionList('default', 'extensions'))
userExtns = self.RemoveKeyBindNames(
self.GetSectionList('user', 'extensions'))
for extn in userExtns:
if extn not in extns: extns.append(extn)
for extn in ('AutoComplete','CodeContext',
'FormatParagraph','ParenMatch'):
extns.remove(extn)
if active_only:
activeExtns = []
for extn in extns:
if self.GetOption('extensions', extn, 'enable', default=True,
type='bool'):
if editor_only or shell_only: if editor_only:
option = "enable_editor"
else:
option = "enable_shell"
if self.GetOption('extensions', extn,option,
default=True, type='bool',
warn_on_default=False):
activeExtns.append(extn)
else:
activeExtns.append(extn)
return activeExtns
else:
return extns
def RemoveKeyBindNames(self, extnNameList):
"Return extnNameList with keybinding section names removed."
return [n for n in extnNameList if not n.endswith(('_bindings', '_cfgBindings'))]
def GetExtnNameForEvent(self, virtualEvent):
extName = None
vEvent = '<<' + virtualEvent + '>>'
for extn in self.GetExtensions(active_only=0):
for event in self.GetExtensionKeys(extn):
if event == vEvent:
extName = extn return extName
def GetExtensionKeys(self, extensionName):
keysName = extensionName + '_cfgBindings'
activeKeys = self.GetCurrentKeySet()
extKeys = {}
if self.defaultCfg['extensions'].has_section(keysName):
eventNames = self.defaultCfg['extensions'].GetOptionList(keysName)
for eventName in eventNames:
event = '<<' + eventName + '>>'
binding = activeKeys[event]
extKeys[event] = binding
return extKeys
def __GetRawExtensionKeys(self,extensionName):
keysName = extensionName+'_cfgBindings'
extKeys = {}
if self.defaultCfg['extensions'].has_section(keysName):
eventNames = self.defaultCfg['extensions'].GetOptionList(keysName)
for eventName in eventNames:
binding = self.GetOption(
'extensions', keysName, eventName, default='').split()
event = '<<' + eventName + '>>'
extKeys[event] = binding
return extKeys
def GetExtensionBindings(self, extensionName):
bindsName = extensionName + '_bindings'
extBinds = self.GetExtensionKeys(extensionName)
if self.defaultCfg['extensions'].has_section(bindsName):
eventNames = self.defaultCfg['extensions'].GetOptionList(bindsName)
for eventName in eventNames:
binding = self.GetOption(
'extensions', bindsName, eventName, default='').split()
event = '<<' + eventName + '>>'
extBinds[event] = binding
return extBinds
def GetKeyBinding(self, keySetName, eventStr):
eventName = eventStr[2:-2] binding = self.GetOption('keys', keySetName, eventName, default='',
warn_on_default=False).split()
return binding
def GetCurrentKeySet(self):
"Return CurrentKeys with 'darwin' modifications."
result = self.GetKeySet(self.CurrentKeys())
if sys.platform == "darwin":
for k, v in result.items():
v2 = [ x.replace('<Alt-', '<Option-') for x in v ]
if v != v2:
result[k] = v2
return result
def GetKeySet(self, keySetName):
keySet = self.GetCoreKeys(keySetName)
activeExtns = self.GetExtensions(active_only=1)
for extn in activeExtns:
extKeys = self.__GetRawExtensionKeys(extn)
if extKeys: for event in extKeys:
if extKeys[event] in keySet.values():
extKeys[event] = '' keySet[event] = extKeys[event] return keySet
def IsCoreBinding(self, virtualEvent):
return ('<<'+virtualEvent+'>>') in self.GetCoreKeys()
former_extension_events = { '<<force-open-completions>>', '<<expand-word>>',
'<<force-open-calltip>>', '<<flash-paren>>', '<<format-paragraph>>',
'<<run-module>>', '<<check-module>>', '<<zoom-height>>',
'<<run-custom>>',
}
def GetCoreKeys(self, keySetName=None):
keyBindings={
'<<copy>>': ['<Control-c>', '<Control-C>'],
'<<cut>>': ['<Control-x>', '<Control-X>'],
'<<paste>>': ['<Control-v>', '<Control-V>'],
'<<beginning-of-line>>': ['<Control-a>', '<Home>'],
'<<center-insert>>': ['<Control-l>'],
'<<close-all-windows>>': ['<Control-q>'],
'<<close-window>>': ['<Alt-F4>'],
'<<do-nothing>>': ['<Control-x>'],
'<<end-of-file>>': ['<Control-d>'],
'<<python-docs>>': ['<F1>'],
'<<python-context-help>>': ['<Shift-F1>'],
'<<history-next>>': ['<Alt-n>'],
'<<history-previous>>': ['<Alt-p>'],
'<<interrupt-execution>>': ['<Control-c>'],
'<<view-restart>>': ['<F6>'],
'<<restart-shell>>': ['<Control-F6>'],
'<<open-class-browser>>': ['<Alt-c>'],
'<<open-module>>': ['<Alt-m>'],
'<<open-new-window>>': ['<Control-n>'],
'<<open-window-from-file>>': ['<Control-o>'],
'<<plain-newline-and-indent>>': ['<Control-j>'],
'<<print-window>>': ['<Control-p>'],
'<<redo>>': ['<Control-y>'],
'<<remove-selection>>': ['<Escape>'],
'<<save-copy-of-window-as-file>>': ['<Alt-Shift-S>'],
'<<save-window-as-file>>': ['<Alt-s>'],
'<<save-window>>': ['<Control-s>'],
'<<select-all>>': ['<Alt-a>'],
'<<toggle-auto-coloring>>': ['<Control-slash>'],
'<<undo>>': ['<Control-z>'],
'<<find-again>>': ['<Control-g>', '<F3>'],
'<<find-in-files>>': ['<Alt-F3>'],
'<<find-selection>>': ['<Control-F3>'],
'<<find>>': ['<Control-f>'],
'<<replace>>': ['<Control-h>'],
'<<goto-line>>': ['<Alt-g>'],
'<<smart-backspace>>': ['<Key-BackSpace>'],
'<<newline-and-indent>>': ['<Key-Return>', '<Key-KP_Enter>'],
'<<smart-indent>>': ['<Key-Tab>'],
'<<indent-region>>': ['<Control-Key-bracketright>'],
'<<dedent-region>>': ['<Control-Key-bracketleft>'],
'<<comment-region>>': ['<Alt-Key-3>'],
'<<uncomment-region>>': ['<Alt-Key-4>'],
'<<tabify-region>>': ['<Alt-Key-5>'],
'<<untabify-region>>': ['<Alt-Key-6>'],
'<<toggle-tabs>>': ['<Alt-Key-t>'],
'<<change-indentwidth>>': ['<Alt-Key-u>'],
'<<del-word-left>>': ['<Control-Key-BackSpace>'],
'<<del-word-right>>': ['<Control-Key-Delete>'],
'<<force-open-completions>>': ['<Control-Key-space>'],
'<<expand-word>>': ['<Alt-Key-slash>'],
'<<force-open-calltip>>': ['<Control-Key-backslash>'],
'<<flash-paren>>': ['<Control-Key-0>'],
'<<format-paragraph>>': ['<Alt-Key-q>'],
'<<run-module>>': ['<Key-F5>'],
'<<run-custom>>': ['<Shift-Key-F5>'],
'<<check-module>>': ['<Alt-Key-x>'],
'<<zoom-height>>': ['<Alt-Key-2>'],
}
if keySetName:
if not (self.userCfg['keys'].has_section(keySetName) or
self.defaultCfg['keys'].has_section(keySetName)):
warning = (
'\n Warning: config.py - IdleConf.GetCoreKeys -\n'
' key set %r is not defined, using default bindings.' %
(keySetName,)
)
_warn(warning, 'keys', keySetName)
else:
for event in keyBindings:
binding = self.GetKeyBinding(keySetName, event)
if binding:
keyBindings[event] = binding
elif event not in self.former_extension_events:
warning = (
'\n Warning: config.py - IdleConf.GetCoreKeys -\n'
' problem retrieving key binding for event %r\n'
' from key set %r.\n'
' returning default value: %r' %
(event, keySetName, keyBindings[event])
)
_warn(warning, 'keys', keySetName, event)
return keyBindings
def GetExtraHelpSourceList(self, configSet):
helpSources = []
if configSet == 'user':
cfgParser = self.userCfg['main']
elif configSet == 'default':
cfgParser = self.defaultCfg['main']
else:
raise InvalidConfigSet('Invalid configSet specified')
options=cfgParser.GetOptionList('HelpFiles')
for option in options:
value=cfgParser.Get('HelpFiles', option, default=';')
if value.find(';') == -1: menuItem = '' helpPath = '' else: value=value.split(';')
menuItem=value[0].strip()
helpPath=value[1].strip()
if menuItem and helpPath: helpSources.append( (menuItem,helpPath,option) )
helpSources.sort(key=lambda x: x[2])
return helpSources
def GetAllExtraHelpSourcesList(self):
allHelpSources = (self.GetExtraHelpSourceList('default') +
self.GetExtraHelpSourceList('user') )
return allHelpSources
def GetFont(self, root, configType, section):
family = self.GetOption(configType, section, 'font', default='courier')
size = self.GetOption(configType, section, 'font-size', type='int',
default='10')
bold = self.GetOption(configType, section, 'font-bold', default=0,
type='bool')
if (family == 'TkFixedFont'):
f = Font(name='TkFixedFont', exists=True, root=root)
actualFont = Font.actual(f)
family = actualFont['family']
size = actualFont['size']
if size <= 0:
size = 10 bold = actualFont['weight'] == 'bold'
return (family, size, 'bold' if bold else 'normal')
def LoadCfgFiles(self):
"Load all configuration files."
for key in self.defaultCfg:
self.defaultCfg[key].Load()
self.userCfg[key].Load()
def SaveUserCfgFiles(self):
"Write all loaded user configuration files to disk."
for key in self.userCfg:
self.userCfg[key].Save()
idleConf = IdleConf()
_warned = set()
def _warn(msg, *key):
key = (msg,) + key
if key not in _warned:
try:
print(msg, file=sys.stderr)
except OSError:
pass
_warned.add(key)
class ConfigChanges(dict):
def __init__(self):
"Create a page for each configuration file"
self.pages = [] for config_type in idleConf.config_types:
self[config_type] = {}
self.pages.append(self[config_type])
def add_option(self, config_type, section, item, value):
"Add item/value pair for config_type and section."
page = self[config_type]
value = str(value) if section not in page:
page[section] = {}
page[section][item] = value
@staticmethod
def save_option(config_type, section, item, value):
if idleConf.defaultCfg[config_type].has_option(section, item):
if idleConf.defaultCfg[config_type].Get(section, item) == value:
return idleConf.userCfg[config_type].RemoveOption(section, item)
return idleConf.userCfg[config_type].SetOption(section, item, value)
def save_all(self):
idleConf.userCfg['main'].Save()
changed = False
for config_type in self:
cfg_type_changed = False
page = self[config_type]
for section in page:
if section == 'HelpFiles': idleConf.userCfg['main'].remove_section('HelpFiles')
cfg_type_changed = True
for item, value in page[section].items():
if self.save_option(config_type, section, item, value):
cfg_type_changed = True
if cfg_type_changed:
idleConf.userCfg[config_type].Save()
changed = True
for config_type in ['keys', 'highlight']:
idleConf.userCfg[config_type].Save()
self.clear()
return changed
def delete_section(self, config_type, section):
if section in self[config_type]:
del self[config_type][section]
configpage = idleConf.userCfg[config_type]
configpage.remove_section(section)
configpage.Save()
def clear(self):
for page in self.pages:
page.clear()
def _dump(): from zlib import crc32
line, crc = 0, 0
def sprint(obj):
nonlocal line, crc
txt = str(obj)
line += 1
crc = crc32(txt.encode(encoding='utf-8'), crc)
print(txt)
def dumpCfg(cfg):
print('\n', cfg, '\n') for key in sorted(cfg):
sections = cfg[key].sections()
sprint(key)
sprint(sections)
for section in sections:
options = cfg[key].options(section)
sprint(section)
sprint(options)
for option in options:
sprint(option + ' = ' + cfg[key].Get(section, option))
dumpCfg(idleConf.defaultCfg)
dumpCfg(idleConf.userCfg)
print('\nlines = ', line, ', crc = ', crc, sep='')
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_config', verbosity=2, exit=False)
_dump()