Skip to content

Commit 8a8c8ee

Browse files
terryjreedyblurb-it[bot]JelleZijlstragpshead
authored
[3.14] gh-89520: Load extension settings and keybindings from user config (GH-28713) (#152992)
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> (cherry picked from commit 208195d)
1 parent f351650 commit 8a8c8ee

6 files changed

Lines changed: 271 additions & 76 deletions

File tree

Lib/idlelib/config.py

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

509533
def GetExtensionBindings(self, extensionName):
510534
"""Return dict {extensionName event : active or defined keybinding}.
@@ -513,18 +537,30 @@ def GetExtensionBindings(self, extensionName):
513537
configurable events (from default config) to GetOption splits,
514538
as in self.__GetRawExtensionKeys.
515539
"""
516-
bindsName = extensionName + '_bindings'
517-
extBinds = self.GetExtensionKeys(extensionName)
518-
#add the non-configurable bindings
519-
if self.defaultCfg['extensions'].has_section(bindsName):
520-
eventNames = self.defaultCfg['extensions'].GetOptionList(bindsName)
521-
for eventName in eventNames:
522-
binding = self.GetOption(
523-
'extensions', bindsName, eventName, default='').split()
524-
event = '<<' + eventName + '>>'
525-
extBinds[event] = binding
526-
527-
return extBinds
540+
bindings_section = f'{extensionName}_bindings'
541+
extension_keys = self.GetExtensionKeys(extensionName)
542+
543+
# add the non-configurable bindings
544+
event_names = set()
545+
if self.userCfg['extensions'].has_section(bindings_section):
546+
event_names |= set(
547+
self.userCfg['extensions'].GetOptionList(bindings_section)
548+
)
549+
if self.defaultCfg['extensions'].has_section(bindings_section):
550+
event_names |= set(
551+
self.defaultCfg['extensions'].GetOptionList(bindings_section)
552+
)
553+
554+
for event_name in event_names:
555+
binding = self.GetOption(
556+
'extensions',
557+
bindings_section,
558+
event_name,
559+
default=''
560+
).split()
561+
event = f'<<{event_name}>>'
562+
extension_keys[event] = binding
563+
return extension_keys
528564

529565
def GetKeyBinding(self, keySetName, eventStr):
530566
"""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.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)