Extension keybindings defined in ~/.idlerc/config-extensions.cfg were silently ignored because GetExtensionKeys, __GetRawExtensionKeys, and GetExtensionBindings only checked default config. Fix these to check user config as well, and update the extensions config dialog to handle user-only extensions correctly.
---------
Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
Co-authored-by: Gregory P. Smith <greg@krypto.org>
Keybindings come from GetCurrentKeySet() active key dict,
where previously used bindings are disabled.
"""
- 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):
+ bindings_section = f'{extensionName}_cfgBindings'
+ current_keyset = self.GetCurrentKeySet()
+ extension_keys = {}
+
+ event_names = set()
+ if self.userCfg['extensions'].has_section(bindings_section):
+ event_names |= set(
+ self.userCfg['extensions'].GetOptionList(bindings_section)
+ )
+ if self.defaultCfg['extensions'].has_section(bindings_section):
+ event_names |= set(
+ self.defaultCfg['extensions'].GetOptionList(bindings_section)
+ )
+
+ for event_name in event_names:
+ event = f'<<{event_name}>>'
+ binding = current_keyset.get(event, None)
+ if binding is None:
+ continue
+ extension_keys[event] = binding
+ return extension_keys
+
+ def __GetRawExtensionKeys(self, extension_name):
"""Return dict {configurable extensionName event : keybinding list}.
Events come from default config extension_cfgBindings section.
Keybindings list come from the splitting of GetOption, which
tries user config before default config.
"""
- 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
+ bindings_section = f'{extension_name}_cfgBindings'
+ extension_keys = {}
+
+ event_names = set()
+ if self.userCfg['extensions'].has_section(bindings_section):
+ event_names |= set(
+ self.userCfg['extensions'].GetOptionList(bindings_section)
+ )
+ if self.defaultCfg['extensions'].has_section(bindings_section):
+ event_names |= set(
+ self.defaultCfg['extensions'].GetOptionList(bindings_section)
+ )
+
+ for event_name in event_names:
+ binding = self.GetOption(
+ 'extensions',
+ bindings_section,
+ event_name,
+ default='',
+ ).split()
+ event = f'<<{event_name}>>'
+ extension_keys[event] = binding
+ return extension_keys
def GetExtensionBindings(self, extensionName):
"""Return dict {extensionName event : active or defined keybinding}.
configurable events (from default config) to GetOption splits,
as in self.__GetRawExtensionKeys.
"""
- bindsName = extensionName + '_bindings'
- extBinds = self.GetExtensionKeys(extensionName)
- #add the non-configurable bindings
- 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
+ bindings_section = f'{extensionName}_bindings'
+ extension_keys = self.GetExtensionKeys(extensionName)
+
+ # add the non-configurable bindings
+ event_names = set()
+ if self.userCfg['extensions'].has_section(bindings_section):
+ event_names |= set(
+ self.userCfg['extensions'].GetOptionList(bindings_section)
+ )
+ if self.defaultCfg['extensions'].has_section(bindings_section):
+ event_names |= set(
+ self.defaultCfg['extensions'].GetOptionList(bindings_section)
+ )
+
+ for event_name in event_names:
+ binding = self.GetOption(
+ 'extensions',
+ bindings_section,
+ event_name,
+ default=''
+ ).split()
+ event = f'<<{event_name}>>'
+ extension_keys[event] = binding
+ return extension_keys
def GetKeyBinding(self, keySetName, eventStr):
"""Return the keybinding list for keySetName eventStr.
def load_extensions(self):
"Fill self.extensions with data from the default and user configs."
self.extensions = {}
+
for ext_name in idleConf.GetExtensions(active_only=False):
# Former built-in extensions are already filtered out.
self.extensions[ext_name] = []
for ext_name in self.extensions:
- opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
+ default = set(self.ext_defaultCfg.GetOptionList(ext_name))
+ user = set(self.ext_userCfg.GetOptionList(ext_name))
+ opt_list = sorted(default | user)
# Bring 'enable' options to the beginning of the list.
enables = [opt_name for opt_name in opt_list
opt_list = enables + opt_list
for opt_name in opt_list:
- def_str = self.ext_defaultCfg.Get(
- ext_name, opt_name, raw=True)
+ if opt_name in default:
+ def_str = self.ext_defaultCfg.Get(
+ ext_name, opt_name, raw=True)
+ else:
+ def_str = self.ext_userCfg.Get(
+ ext_name, opt_name, raw=True)
try:
def_obj = {'True':True, 'False':False}[def_str]
opt_type = 'bool'
default = opt['default']
value = opt['var'].get().strip() or default
opt['var'].set(value)
- # if self.defaultCfg.has_section(section):
- # Currently, always true; if not, indent to return.
- if (value == default):
+
+ # Only save option in user config if it differs from the default
+ if self.ext_defaultCfg.has_section(section) and value == default:
return self.ext_userCfg.RemoveOption(section, name)
+
# Set the option.
return self.ext_userCfg.SetOption(section, name, value)
self.text.event_delete(event, *keylist)
for extensionName in self.get_standard_extension_names():
xkeydefs = idleConf.GetExtensionBindings(extensionName)
- if xkeydefs:
- for event, keylist in xkeydefs.items():
- self.text.event_delete(event, *keylist)
+ for event, keylist in xkeydefs.items():
+ self.text.event_delete(event, *keylist)
def ApplyKeybindings(self):
"""Apply the virtual, configurable keybindings.
self.text.undo_block_stop = mock.Mock()
-class ZZDummyTest(unittest.TestCase):
-
- @classmethod
- def setUpClass(cls):
- requires('gui')
- root = cls.root = Tk()
- root.withdraw()
- text = cls.text = Text(cls.root)
- cls.editor = DummyEditwin(root, text)
- zzdummy.idleConf.userCfg = testcfg
-
- @classmethod
- def tearDownClass(cls):
- zzdummy.idleConf.userCfg = usercfg
- del cls.editor, cls.text
- cls.root.update_idletasks()
- for id in cls.root.tk.call('after', 'info'):
- cls.root.after_cancel(id) # Need for EditorWindow.
- cls.root.destroy()
- del cls.root
-
- def setUp(self):
- text = self.text
- text.insert('1.0', code_sample)
- text.undo_block_start.reset_mock()
- text.undo_block_stop.reset_mock()
- zz = self.zz = zzdummy.ZzDummy(self.editor)
- zzdummy.ZzDummy.ztext = '# ignore #'
-
- def tearDown(self):
- self.text.delete('1.0', 'end')
- del self.zz
+class ZZDummyMixin:
+ """Shared tests for ZzDummy with default and user configs."""
def checklines(self, text, value):
# Verify that there are lines being checked.
def test_reload(self):
self.assertEqual(self.zz.ztext, '# ignore #')
- testcfg['extensions'].SetOption('ZzDummy', 'z-text', 'spam')
+ zzdummy.idleConf.userCfg['extensions'].SetOption(
+ 'ZzDummy', 'z-text', 'spam')
zzdummy.ZzDummy.reload()
self.assertEqual(self.zz.ztext, 'spam')
self.assertEqual(text.get('1.0', 'end-1c'), code_sample)
+class ZZDummyTest(ZZDummyMixin, unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ requires('gui')
+ root = cls.root = Tk()
+ root.withdraw()
+ text = cls.text = Text(cls.root)
+ cls.editor = DummyEditwin(root, text)
+ zzdummy.idleConf.userCfg = testcfg
+
+ @classmethod
+ def tearDownClass(cls):
+ zzdummy.idleConf.userCfg = usercfg
+ del cls.editor, cls.text
+ cls.root.update_idletasks()
+ for id in cls.root.tk.call('after', 'info'):
+ cls.root.after_cancel(id) # Need for EditorWindow.
+ cls.root.destroy()
+ del cls.root
+
+ def setUp(self):
+ text = self.text
+ text.insert('1.0', code_sample)
+ text.undo_block_start.reset_mock()
+ text.undo_block_stop.reset_mock()
+ zz = self.zz = zzdummy.ZzDummy(self.editor)
+ zzdummy.ZzDummy.ztext = '# ignore #'
+
+ def tearDown(self):
+ self.text.delete('1.0', 'end')
+ del self.zz
+
+ def test_exists(self):
+ conf = zzdummy.idleConf
+ self.assertEqual(
+ conf.GetSectionList('user', 'extensions'), [])
+ self.assertEqual(
+ conf.GetSectionList('default', 'extensions'),
+ ['AutoComplete', 'CodeContext', 'FormatParagraph',
+ 'ParenMatch', 'ZzDummy', 'ZzDummy_cfgBindings',
+ 'ZzDummy_bindings'])
+ self.assertIn("ZzDummy", conf.GetExtensions(False))
+ self.assertNotIn("ZzDummy", conf.GetExtensions())
+ self.assertEqual(
+ conf.GetExtensionKeys("ZzDummy"), {})
+ self.assertEqual(
+ conf.GetExtensionBindings("ZzDummy"),
+ {'<<z-out>>': ['<Control-Shift-KeyRelease-Delete>']})
+
+ def test_exists_user(self):
+ conf = zzdummy.idleConf
+ conf.userCfg["extensions"].read_dict({
+ "ZzDummy": {'enable': 'True'}
+ })
+ self.assertEqual(
+ conf.GetSectionList('user', 'extensions'),
+ ["ZzDummy"])
+ self.assertIn("ZzDummy", conf.GetExtensions())
+ self.assertEqual(
+ conf.GetExtensionKeys("ZzDummy"),
+ {'<<z-in>>': ['<Control-Shift-KeyRelease-Insert>']})
+ self.assertEqual(
+ conf.GetExtensionBindings("ZzDummy"),
+ {'<<z-in>>': ['<Control-Shift-KeyRelease-Insert>'],
+ '<<z-out>>': ['<Control-Shift-KeyRelease-Delete>']})
+ # Restore
+ conf.userCfg["extensions"].remove_section("ZzDummy")
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
--- /dev/null
+"Test zzdummy with user config, coverage 100%."
+
+from idlelib import zzdummy
+import unittest
+from test.support import requires
+from tkinter import Tk, Text
+from idlelib import config
+
+from idlelib.idle_test.test_zzdummy import (
+ ZZDummyMixin, DummyEditwin, code_sample,
+)
+
+
+real_usercfg = zzdummy.idleConf.userCfg
+test_usercfg = {
+ 'main': config.IdleUserConfParser(''),
+ 'highlight': config.IdleUserConfParser(''),
+ 'keys': config.IdleUserConfParser(''),
+ 'extensions': config.IdleUserConfParser(''),
+}
+test_usercfg["extensions"].read_dict({
+ "ZzDummy": {'enable': 'True', 'enable_shell': 'False',
+ 'enable_editor': 'True', 'z-text': 'Z'},
+ "ZzDummy_cfgBindings": {
+ 'z-in': '<Control-Shift-KeyRelease-Insert>'},
+ "ZzDummy_bindings": {
+ 'z-out': '<Control-Shift-KeyRelease-Delete>'},
+})
+real_defaultcfg = zzdummy.idleConf.defaultCfg
+test_defaultcfg = {
+ 'main': config.IdleUserConfParser(''),
+ 'highlight': config.IdleUserConfParser(''),
+ 'keys': config.IdleUserConfParser(''),
+ 'extensions': config.IdleUserConfParser(''),
+}
+test_defaultcfg["extensions"].read_dict({
+ "AutoComplete": {'popupwait': '2000'},
+ "CodeContext": {'maxlines': '15'},
+ "FormatParagraph": {'max-width': '72'},
+ "ParenMatch": {'style': 'expression',
+ 'flash-delay': '500', 'bell': 'True'},
+})
+test_defaultcfg["main"].read_dict({
+ "Theme": {"default": 1, "name": "IDLE Classic", "name2": ""},
+ "Keys": {"default": 1, "name": "IDLE Classic", "name2": ""},
+})
+for key in ("keys",):
+ real_default = real_defaultcfg[key]
+ value = {name: dict(real_default[name]) for name in real_default}
+ test_defaultcfg[key].read_dict(value)
+
+
+class ZZDummyTest(ZZDummyMixin, unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ requires('gui')
+ root = cls.root = Tk()
+ root.withdraw()
+ text = cls.text = Text(cls.root)
+ cls.editor = DummyEditwin(root, text)
+ zzdummy.idleConf.userCfg = test_usercfg
+ zzdummy.idleConf.defaultCfg = test_defaultcfg
+
+ @classmethod
+ def tearDownClass(cls):
+ zzdummy.idleConf.defaultCfg = real_defaultcfg
+ zzdummy.idleConf.userCfg = real_usercfg
+ del cls.editor, cls.text
+ cls.root.update_idletasks()
+ for id in cls.root.tk.call('after', 'info'):
+ cls.root.after_cancel(id) # Need for EditorWindow.
+ cls.root.destroy()
+ del cls.root
+
+ def setUp(self):
+ text = self.text
+ text.insert('1.0', code_sample)
+ text.undo_block_start.reset_mock()
+ text.undo_block_stop.reset_mock()
+ zz = self.zz = zzdummy.ZzDummy(self.editor)
+ zzdummy.ZzDummy.ztext = '# ignore #'
+
+ def tearDown(self):
+ self.text.delete('1.0', 'end')
+ del self.zz
+
+ def test_exists(self):
+ self.assertEqual(
+ zzdummy.idleConf.GetSectionList('user', 'extensions'),
+ ['ZzDummy', 'ZzDummy_cfgBindings', 'ZzDummy_bindings'])
+ self.assertEqual(
+ zzdummy.idleConf.GetSectionList('default', 'extensions'),
+ ['AutoComplete', 'CodeContext', 'FormatParagraph',
+ 'ParenMatch'])
+ self.assertIn("ZzDummy",
+ zzdummy.idleConf.GetExtensions())
+ self.assertEqual(
+ zzdummy.idleConf.GetExtensionKeys("ZzDummy"),
+ {'<<z-in>>': ['<Control-Shift-KeyRelease-Insert>']})
+ self.assertEqual(
+ zzdummy.idleConf.GetExtensionBindings("ZzDummy"),
+ {'<<z-in>>': ['<Control-Shift-KeyRelease-Insert>'],
+ '<<z-out>>': ['<Control-Shift-KeyRelease-Delete>']})
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
--- /dev/null
+Make IDLE extension configuration look at user config files, allowing
+user-installed extensions to have settings and key bindings defined in
+~/.idlerc.