Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
354ad86
Make extentions use user's keys, not all defaults
CoolCat467 Oct 3, 2021
b6d2782
📜🤖 Added by blurb_it.
blurb-it[bot] Oct 3, 2021
48e18d6
Fix config and dialog to use user config extentions
CoolCat467 Oct 4, 2021
d8a267c
Merge branch 'python:main' into patch-1
CoolCat467 Oct 4, 2021
e1e8cd9
Remove debug print I forgot
CoolCat467 Oct 6, 2021
e2a2528
Remove debug comment out lines
CoolCat467 Oct 6, 2021
8fe6ecd
Merge branch 'python:main' into patch-1
CoolCat467 Oct 7, 2021
539d63b
Merge branch 'python:main' into patch-1
CoolCat467 Oct 16, 2021
1dfacb9
Merge branch 'python:main' into patch-1
CoolCat467 Nov 11, 2021
4e2161b
Fix spelling
CoolCat467 Feb 1, 2022
b425b72
Revert previous change
CoolCat467 Feb 1, 2022
88d40e0
Merge branch 'python:main' into patch-1
CoolCat467 May 24, 2022
c835095
Merge branch 'main' into patch-1
CoolCat467 Aug 27, 2023
f0e856a
Add trailing newline
CoolCat467 Aug 27, 2023
54b6c54
Merge branch 'main' into patch-1
CoolCat467 Aug 27, 2023
73c984e
Merge branch 'python:main' into patch-1
CoolCat467 Nov 11, 2023
40d864b
Simplify and update from `idleuserextend`'s testing
CoolCat467 Nov 11, 2023
fc190e3
Revert always save changes
CoolCat467 Nov 11, 2023
a2eb4b6
Merge branch 'python:main' into patch-1
CoolCat467 Nov 14, 2023
dc46fb7
Revert unnecessary formatting changes
CoolCat467 Nov 17, 2023
59b6a25
Merge branch 'main' into patch-1
CoolCat467 Nov 17, 2023
016ce89
`user_list` -> `user`, as is no longer a list but a set.
CoolCat467 Nov 17, 2023
ba7218f
Merge branch 'main' into patch-1
CoolCat467 Jan 26, 2024
954d0b8
Merge branch 'main' into patch-1
CoolCat467 Mar 11, 2024
568cd80
Revert noted strictly format change
CoolCat467 Mar 12, 2024
0240959
Remove suppression for errors that will never happen
CoolCat467 Mar 12, 2024
c032cf8
Merge branch 'main' into patch-1
CoolCat467 Mar 12, 2024
2f47ecb
Add test for ZzDummy being loaded in extensions list
CoolCat467 Mar 12, 2024
3a9d1dd
Don't require enabled
CoolCat467 Mar 12, 2024
29961f3
Add tests for reading extension keys
CoolCat467 Mar 12, 2024
8336d96
Merge branch 'main' into patch-1
CoolCat467 Mar 29, 2024
350253f
Merge branch 'main' into patch-1
CoolCat467 Apr 6, 2024
d6c8947
Merge branch 'main' into patch-1
CoolCat467 May 7, 2024
e521eef
Merge branch 'main' into patch-1
CoolCat467 May 12, 2024
9e04329
Merge branch 'main' into patch-1
CoolCat467 Aug 16, 2024
c0aaf33
Merge branch 'main' into patch-1
CoolCat467 Oct 8, 2024
e142132
Merge branch 'main' into patch-1
CoolCat467 Nov 6, 2024
455e8df
Merge branch 'main' into patch-1
CoolCat467 Dec 23, 2024
344d555
Merge branch 'main' into patch-1
CoolCat467 Feb 24, 2025
d814f03
Merge branch 'main' into patch-1
CoolCat467 Apr 18, 2025
7d0f5ac
Merge branch 'main' into patch-1
CoolCat467 Aug 1, 2025
b074c87
Merge branch 'main' into patch-1
CoolCat467 Dec 17, 2025
71b544e
Merge branch 'main' into patch-1
CoolCat467 Mar 23, 2026
3491d5a
Merge branch 'main' into patch-1
CoolCat467 Apr 12, 2026
eb194c3
Merge branch 'main' into patch-1
gpshead Apr 12, 2026
cf1b35f
gh-89520: IDLE - Replace ChainMap misuse with set union in config.py
gpshead Apr 12, 2026
5a2d995
gh-89520: IDLE - Use self instead of module-level idleConf in GetExte…
gpshead Apr 12, 2026
c257eda
gh-89520: IDLE - Fix default value logic in configdialog load_extensions
gpshead Apr 12, 2026
392985a
gh-89520: IDLE - Update NEWS entry to gh-issue format and fix content
gpshead Apr 12, 2026
4f9e553
gh-89520: IDLE - Fix test_exists_user cleanup to properly remove section
gpshead Apr 12, 2026
738d6cb
gh-89520: IDLE - Factor out shared test code from zzdummy test files
gpshead Apr 12, 2026
54c16dd
gh-89520: IDLE - Wrap long lines in zzdummy test files
gpshead Apr 12, 2026
5fb7df7
gh-89520: IDLE - Preserve public API parameter names in config.py
gpshead Apr 12, 2026
f47e3e3
address terryjreedy's comment, no need for the split loop
gpshead Apr 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 70 additions & 34 deletions Lib/idlelib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,34 +476,58 @@ def GetExtensionKeys(self, extensionName):
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}.
Expand All @@ -512,18 +536,30 @@ def GetExtensionBindings(self, extensionName):
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.
Expand Down
20 changes: 14 additions & 6 deletions Lib/idlelib/configdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -1960,12 +1960,15 @@ def create_page_extensions(self):
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
Expand All @@ -1975,8 +1978,12 @@ def load_extensions(self):
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'
Expand Down Expand Up @@ -2054,10 +2061,11 @@ def set_extension_value(self, section, opt):
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)

Expand Down
5 changes: 2 additions & 3 deletions Lib/idlelib/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -860,9 +860,8 @@ def RemoveKeybindings(self):
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.
Expand Down
107 changes: 74 additions & 33 deletions Lib/idlelib/idle_test/test_zzdummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,38 +38,8 @@ def __init__(self, root, text):
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.
Expand All @@ -89,7 +59,8 @@ def test_init(self):

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

Expand Down Expand Up @@ -148,5 +119,75 @@ def test_roundtrip(self):
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)
Loading
Loading