Skip to content

Commit 208195d

Browse files
CoolCat467blurb-it[bot]JelleZijlstragpshead
authored
gh-89520: Load extension settings and keybindings from user config (GH-28713)
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>
1 parent b3b0cef commit 208195d

File tree

6 files changed

+271
-76
lines changed

6 files changed

+271
-76
lines changed

Lib/idlelib/config.py

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -476,34 +476,58 @@ def GetExtensionKeys(self, extensionName):
476476
Keybindings come from GetCurrentKeySet() active key dict,
477477
where previously used bindings are disabled.
478478
"""
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):
479+
bindings_section = f'{extensionName}_cfgBindings'
480+
current_keyset = self.GetCurrentKeySet()
481+
extension_keys = {}
482+
483+
event_names = set()
484+
if self.userCfg['extensions'].has_section(bindings_section):
485+
event_names |= set(
486+
self.userCfg['extensions'].GetOptionList(bindings_section)
487+
)
488+
if self.defaultCfg['extensions'].has_section(bindings_section):
489+
event_names |= set(
490+
self.defaultCfg['extensions'].GetOptionList(bindings_section)
491+
)
492+
493+
for event_name in event_names:
494+
event = f'<<{event_name}>>'
495+
binding = current_keyset.get(event, None)
496+
if binding is None:
497+
continue
498+
extension_keys[event] = binding
499+
return extension_keys
500+
501+
def __GetRawExtensionKeys(self, extension_name):
491502
"""Return dict {configurable extensionName event : keybinding list}.
492503
493504
Events come from default config extension_cfgBindings section.
494505
Keybindings list come from the splitting of GetOption, which
495506
tries user config before default config.
496507
"""
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
508+
bindings_section = f'{extension_name}_cfgBindings'
509+
extension_keys = {}
510+
511+
event_names = set()
512+
if self.userCfg['extensions'].has_section(bindings_section):
513+
event_names |= set(
514+
self.userCfg['extensions'].GetOptionList(bindings_section)
515+
)
516+
if self.defaultCfg['extensions'].has_section(bindings_section):
517+
event_names |= set(
518+
self.defaultCfg['extensions'].GetOptionList(bindings_section)
519+
)
520+
521+
for event_name in event_names:
522+
binding = self.GetOption(
523+
'extensions',
524+
bindings_section,
525+
event_name,
526+
default='',
527+
).split()
528+
event = f'<<{event_name}>>'
529+
extension_keys[event] = binding
530+
return extension_keys
507531

508532
def GetExtensionBindings(self, extensionName):
509533
"""Return dict {extensionName event : active or defined keybinding}.
@@ -512,18 +536,30 @@ def GetExtensionBindings(self, extensionName):
512536
configurable events (from default config) to GetOption splits,
513537
as in self.__GetRawExtensionKeys.
514538
"""
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
539+
bindings_section = f'{extensionName}_bindings'
540+
extension_keys = self.GetExtensionKeys(extensionName)
541+
542+
# add the non-configurable bindings
543+
event_names = set()
544+
if self.userCfg['extensions'].has_section(bindings_section):
545+
event_names |= set(
546+
self.userCfg['extensions'].GetOptionList(bindings_section)
547+
)
548+
if self.defaultCfg['extensions'].has_section(bindings_section):
549+
event_names |= set(
550+
self.defaultCfg['extensions'].GetOptionList(bindings_section)
551+
)
552+
553+
for event_name in event_names:
554+
binding = self.GetOption(
555+
'extensions',
556+
bindings_section,
557+
event_name,
558+
default=''
559+
).split()
560+
event = f'<<{event_name}>>'
561+
extension_keys[event] = binding
562+
return extension_keys
527563

528564
def GetKeyBinding(self, keySetName, eventStr):
529565
"""Return the keybinding list for keySetName eventStr.

Lib/idlelib/configdialog.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1960,12 +1960,15 @@ def create_page_extensions(self):
19601960
def load_extensions(self):
19611961
"Fill self.extensions with data from the default and user configs."
19621962
self.extensions = {}
1963+
19631964
for ext_name in idleConf.GetExtensions(active_only=False):
19641965
# Former built-in extensions are already filtered out.
19651966
self.extensions[ext_name] = []
19661967

19671968
for ext_name in self.extensions:
1968-
opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
1969+
default = set(self.ext_defaultCfg.GetOptionList(ext_name))
1970+
user = set(self.ext_userCfg.GetOptionList(ext_name))
1971+
opt_list = sorted(default | user)
19691972

19701973
# Bring 'enable' options to the beginning of the list.
19711974
enables = [opt_name for opt_name in opt_list
@@ -1975,8 +1978,12 @@ def load_extensions(self):
19751978
opt_list = enables + opt_list
19761979

19771980
for opt_name in opt_list:
1978-
def_str = self.ext_defaultCfg.Get(
1979-
ext_name, opt_name, raw=True)
1981+
if opt_name in default:
1982+
def_str = self.ext_defaultCfg.Get(
1983+
ext_name, opt_name, raw=True)
1984+
else:
1985+
def_str = self.ext_userCfg.Get(
1986+
ext_name, opt_name, raw=True)
19801987
try:
19811988
def_obj = {'True':True, 'False':False}[def_str]
19821989
opt_type = 'bool'
@@ -2054,10 +2061,11 @@ def set_extension_value(self, section, opt):
20542061
default = opt['default']
20552062
value = opt['var'].get().strip() or default
20562063
opt['var'].set(value)
2057-
# if self.defaultCfg.has_section(section):
2058-
# Currently, always true; if not, indent to return.
2059-
if (value == default):
2064+
2065+
# Only save option in user config if it differs from the default
2066+
if self.ext_defaultCfg.has_section(section) and value == default:
20602067
return self.ext_userCfg.RemoveOption(section, name)
2068+
20612069
# Set the option.
20622070
return self.ext_userCfg.SetOption(section, name, value)
20632071

Lib/idlelib/editor.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -860,9 +860,8 @@ def RemoveKeybindings(self):
860860
self.text.event_delete(event, *keylist)
861861
for extensionName in self.get_standard_extension_names():
862862
xkeydefs = idleConf.GetExtensionBindings(extensionName)
863-
if xkeydefs:
864-
for event, keylist in xkeydefs.items():
865-
self.text.event_delete(event, *keylist)
863+
for event, keylist in xkeydefs.items():
864+
self.text.event_delete(event, *keylist)
866865

867866
def ApplyKeybindings(self):
868867
"""Apply the virtual, configurable keybindings.

Lib/idlelib/idle_test/test_zzdummy.py

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -38,38 +38,8 @@ def __init__(self, root, text):
3838
self.text.undo_block_stop = mock.Mock()
3939

4040

41-
class ZZDummyTest(unittest.TestCase):
42-
43-
@classmethod
44-
def setUpClass(cls):
45-
requires('gui')
46-
root = cls.root = Tk()
47-
root.withdraw()
48-
text = cls.text = Text(cls.root)
49-
cls.editor = DummyEditwin(root, text)
50-
zzdummy.idleConf.userCfg = testcfg
51-
52-
@classmethod
53-
def tearDownClass(cls):
54-
zzdummy.idleConf.userCfg = usercfg
55-
del cls.editor, cls.text
56-
cls.root.update_idletasks()
57-
for id in cls.root.tk.call('after', 'info'):
58-
cls.root.after_cancel(id) # Need for EditorWindow.
59-
cls.root.destroy()
60-
del cls.root
61-
62-
def setUp(self):
63-
text = self.text
64-
text.insert('1.0', code_sample)
65-
text.undo_block_start.reset_mock()
66-
text.undo_block_stop.reset_mock()
67-
zz = self.zz = zzdummy.ZzDummy(self.editor)
68-
zzdummy.ZzDummy.ztext = '# ignore #'
69-
70-
def tearDown(self):
71-
self.text.delete('1.0', 'end')
72-
del self.zz
41+
class ZZDummyMixin:
42+
"""Shared tests for ZzDummy with default and user configs."""
7343

7444
def checklines(self, text, value):
7545
# Verify that there are lines being checked.
@@ -89,7 +59,8 @@ def test_init(self):
8959

9060
def test_reload(self):
9161
self.assertEqual(self.zz.ztext, '# ignore #')
92-
testcfg['extensions'].SetOption('ZzDummy', 'z-text', 'spam')
62+
zzdummy.idleConf.userCfg['extensions'].SetOption(
63+
'ZzDummy', 'z-text', 'spam')
9364
zzdummy.ZzDummy.reload()
9465
self.assertEqual(self.zz.ztext, 'spam')
9566

@@ -148,5 +119,75 @@ def test_roundtrip(self):
148119
self.assertEqual(text.get('1.0', 'end-1c'), code_sample)
149120

150121

122+
class ZZDummyTest(ZZDummyMixin, unittest.TestCase):
123+
124+
@classmethod
125+
def setUpClass(cls):
126+
requires('gui')
127+
root = cls.root = Tk()
128+
root.withdraw()
129+
text = cls.text = Text(cls.root)
130+
cls.editor = DummyEditwin(root, text)
131+
zzdummy.idleConf.userCfg = testcfg
132+
133+
@classmethod
134+
def tearDownClass(cls):
135+
zzdummy.idleConf.userCfg = usercfg
136+
del cls.editor, cls.text
137+
cls.root.update_idletasks()
138+
for id in cls.root.tk.call('after', 'info'):
139+
cls.root.after_cancel(id) # Need for EditorWindow.
140+
cls.root.destroy()
141+
del cls.root
142+
143+
def setUp(self):
144+
text = self.text
145+
text.insert('1.0', code_sample)
146+
text.undo_block_start.reset_mock()
147+
text.undo_block_stop.reset_mock()
148+
zz = self.zz = zzdummy.ZzDummy(self.editor)
149+
zzdummy.ZzDummy.ztext = '# ignore #'
150+
151+
def tearDown(self):
152+
self.text.delete('1.0', 'end')
153+
del self.zz
154+
155+
def test_exists(self):
156+
conf = zzdummy.idleConf
157+
self.assertEqual(
158+
conf.GetSectionList('user', 'extensions'), [])
159+
self.assertEqual(
160+
conf.GetSectionList('default', 'extensions'),
161+
['AutoComplete', 'CodeContext', 'FormatParagraph',
162+
'ParenMatch', 'ZzDummy', 'ZzDummy_cfgBindings',
163+
'ZzDummy_bindings'])
164+
self.assertIn("ZzDummy", conf.GetExtensions(False))
165+
self.assertNotIn("ZzDummy", conf.GetExtensions())
166+
self.assertEqual(
167+
conf.GetExtensionKeys("ZzDummy"), {})
168+
self.assertEqual(
169+
conf.GetExtensionBindings("ZzDummy"),
170+
{'<<z-out>>': ['<Control-Shift-KeyRelease-Delete>']})
171+
172+
def test_exists_user(self):
173+
conf = zzdummy.idleConf
174+
conf.userCfg["extensions"].read_dict({
175+
"ZzDummy": {'enable': 'True'}
176+
})
177+
self.assertEqual(
178+
conf.GetSectionList('user', 'extensions'),
179+
["ZzDummy"])
180+
self.assertIn("ZzDummy", conf.GetExtensions())
181+
self.assertEqual(
182+
conf.GetExtensionKeys("ZzDummy"),
183+
{'<<z-in>>': ['<Control-Shift-KeyRelease-Insert>']})
184+
self.assertEqual(
185+
conf.GetExtensionBindings("ZzDummy"),
186+
{'<<z-in>>': ['<Control-Shift-KeyRelease-Insert>'],
187+
'<<z-out>>': ['<Control-Shift-KeyRelease-Delete>']})
188+
# Restore
189+
conf.userCfg["extensions"].remove_section("ZzDummy")
190+
191+
151192
if __name__ == '__main__':
152193
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)