diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py index d10c88a43f9231..0291bf31fb8e26 100644 --- a/Lib/idlelib/config.py +++ b/Lib/idlelib/config.py @@ -28,6 +28,7 @@ from configparser import ConfigParser import os import sys +from collections import ChainMap from tkinter.font import Font import idlelib @@ -469,61 +470,95 @@ def GetExtnNameForEvent(self, virtualEvent): extName = extn # TODO return here? return extName - def GetExtensionKeys(self, extensionName): + def GetExtensionKeys(self, extension_name): """Return dict: {configurable extensionName event : active keybinding}. Events come from default config extension_cfgBindings section. 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'{extension_name}_cfgBindings' + current_keyset = idleConf.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 - - def GetExtensionBindings(self, extensionName): + bindings_section = f'{extension_name}_cfgBindings' + extension_keys = {} + + event_names = [] + if self.userCfg['extensions'].has_section(bindings_section): + event_names.append(self.userCfg['extensions'].GetOptionList(bindings_section)) + if self.defaultCfg['extensions'].has_section(bindings_section): + event_names.append(self.defaultCfg['extensions'].GetOptionList(bindings_section)) + + # Because chain map, favors user bindings over default bindings + for event_name in ChainMap(*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, extension_name): """Return dict {extensionName event : active or defined keybinding}. Augment self.GetExtensionKeys(extensionName) with mapping of non- 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'{extension_name}_bindings' + extension_keys = self.GetExtensionKeys(extension_name) + + # add the non-configurable bindings + values = [] + if self.userCfg['extensions'].has_section(bindings_section): + values.append( + self.userCfg['extensions'].GetOptionList(bindings_section) + ) + if self.defaultCfg['extensions'].has_section(bindings_section): + values.append( + self.defaultCfg['extensions'].GetOptionList(bindings_section) + ) + + # Because chain map, favors user bindings over default bindings + for event_name in ChainMap(*values): + 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. @@ -758,7 +793,8 @@ def LoadCfgFiles(self): "Load all configuration files." for key in self.defaultCfg: self.defaultCfg[key].Load() - self.userCfg[key].Load() #same keys + for key in self.userCfg: + self.userCfg[key].Load() def SaveUserCfgFiles(self): "Write all loaded user configuration files to disk." diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index e618ef07a90271..d072646a010dac 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -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 @@ -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 user: + def_str = self.ext_userCfg.Get( + ext_name, opt_name, raw=True) + else: + def_str = self.ext_defaultCfg.Get( + ext_name, opt_name, raw=True) try: def_obj = {'True':True, 'False':False}[def_str] opt_type = 'bool' @@ -1988,9 +1995,14 @@ def load_extensions(self): def_obj = def_str opt_type = None try: - value = self.ext_userCfg.Get( - ext_name, opt_name, type=opt_type, raw=True, - default=def_obj) + if opt_name in user: + value = self.ext_userCfg.Get( + ext_name, opt_name, type=opt_type, raw=True, + default=def_obj) + else: + value = self.ext_defaultCfg.Get( + ext_name, opt_name, type=opt_type, raw=True, + default=def_obj) except ValueError: # Need this until .Get fixed. value = def_obj # Bad values overwritten by entry. var = StringVar(self) @@ -2054,10 +2066,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) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 17b498f63ba43b..cdf99a3787e028 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -907,9 +907,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. diff --git a/Lib/idlelib/idle_test/test_zzdummy.py b/Lib/idlelib/idle_test/test_zzdummy.py index 209d8564da0664..3ca3ed53d00f5c 100644 --- a/Lib/idlelib/idle_test/test_zzdummy.py +++ b/Lib/idlelib/idle_test/test_zzdummy.py @@ -82,6 +82,27 @@ def checklines(self, text, value): actual.append(txt.startswith(value)) return actual + def test_exists(self): + self.assertEqual(zzdummy.idleConf.GetSectionList('user', 'extensions'), []) + self.assertEqual(zzdummy.idleConf.GetSectionList('default', 'extensions'), ['AutoComplete', 'CodeContext', 'FormatParagraph', 'ParenMatch', 'ZzDummy', 'ZzDummy_cfgBindings', 'ZzDummy_bindings']) + self.assertIn("ZzDummy", zzdummy.idleConf.GetExtensions(False)) + self.assertNotIn("ZzDummy", zzdummy.idleConf.GetExtensions()) + self.assertEqual(zzdummy.idleConf.GetExtensionKeys("ZzDummy"), {}) + self.assertEqual(zzdummy.idleConf.GetExtensionBindings("ZzDummy"), {'<>': ['']}) + + def test_exists_user(self): + zzdummy.idleConf.userCfg["extensions"].read_dict({ + "ZzDummy": {'enable': 'True'} + }) + self.assertEqual(zzdummy.idleConf.GetSectionList('user', 'extensions'), ["ZzDummy"]) + self.assertIn("ZzDummy", zzdummy.idleConf.GetExtensions()) + self.assertEqual(zzdummy.idleConf.GetExtensionKeys("ZzDummy"), {'<>': ['']}) + self.assertEqual(zzdummy.idleConf.GetExtensionBindings("ZzDummy"), {'<>': [''], '<>': ['']}) + # Restore + zzdummy.idleConf.userCfg["extensions"].read_dict({ + "ZzDummy": {'enable': 'False'} + }) + def test_init(self): zz = self.zz self.assertEqual(zz.editwin, self.editor) diff --git a/Lib/idlelib/idle_test/test_zzdummy_user.py b/Lib/idlelib/idle_test/test_zzdummy_user.py new file mode 100644 index 00000000000000..ebe401bc847a24 --- /dev/null +++ b/Lib/idlelib/idle_test/test_zzdummy_user.py @@ -0,0 +1,187 @@ +"Test zzdummy, coverage 100%." + +from idlelib import zzdummy +import unittest +from test.support import requires +from tkinter import Tk, Text +from unittest import mock +from idlelib import config +from idlelib import editor +from idlelib import format + + +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': ''}, + "ZzDummy_bindings": {'z-out': ''}, +}) +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) +code_sample = """\ + +class C1: + # Class comment. + def __init__(self, a, b): + self.a = a + self.b = b +""" + + +class DummyEditwin: + get_selection_indices = editor.EditorWindow.get_selection_indices + def __init__(self, root, text): + self.root = root + self.top = root + self.text = text + self.fregion = format.FormatRegion(self) + self.text.undo_block_start = mock.Mock() + 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 = 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 checklines(self, text, value): + # Verify that there are lines being checked. + end_line = int(float(text.index('end'))) + + # Check each line for the starting text. + actual = [] + for line in range(1, end_line): + txt = text.get(f'{line}.0', f'{line}.end') + actual.append(txt.startswith(value)) + return actual + + 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"), {'<>': ['']}) + self.assertEqual(zzdummy.idleConf.GetExtensionBindings("ZzDummy"), {'<>': [''], '<>': ['']}) + + def test_init(self): + zz = self.zz + self.assertEqual(zz.editwin, self.editor) + self.assertEqual(zz.text, self.editor.text) + + def test_reload(self): + self.assertEqual(self.zz.ztext, '# ignore #') + test_usercfg['extensions'].SetOption('ZzDummy', 'z-text', 'spam') + zzdummy.ZzDummy.reload() + self.assertEqual(self.zz.ztext, 'spam') + + def test_z_in_event(self): + eq = self.assertEqual + zz = self.zz + text = zz.text + eq(self.zz.ztext, '# ignore #') + + # No lines have the leading text. + expected = [False, False, False, False, False, False, False] + actual = self.checklines(text, zz.ztext) + eq(expected, actual) + + text.tag_add('sel', '2.0', '4.end') + eq(zz.z_in_event(), 'break') + expected = [False, True, True, True, False, False, False] + actual = self.checklines(text, zz.ztext) + eq(expected, actual) + + text.undo_block_start.assert_called_once() + text.undo_block_stop.assert_called_once() + + def test_z_out_event(self): + eq = self.assertEqual + zz = self.zz + text = zz.text + eq(self.zz.ztext, '# ignore #') + + # Prepend text. + text.tag_add('sel', '2.0', '5.end') + zz.z_in_event() + text.undo_block_start.reset_mock() + text.undo_block_stop.reset_mock() + + # Select a few lines to remove text. + text.tag_remove('sel', '1.0', 'end') + text.tag_add('sel', '3.0', '4.end') + eq(zz.z_out_event(), 'break') + expected = [False, True, False, False, True, False, False] + actual = self.checklines(text, zz.ztext) + eq(expected, actual) + + text.undo_block_start.assert_called_once() + text.undo_block_stop.assert_called_once() + + def test_roundtrip(self): + # Insert and remove to all code should give back original text. + zz = self.zz + text = zz.text + + text.tag_add('sel', '1.0', 'end-1c') + zz.z_in_event() + zz.z_out_event() + + self.assertEqual(text.get('1.0', 'end-1c'), code_sample) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Misc/NEWS.d/next/IDLE/2021-10-03-21-55-34.bpo-45357.etEExa.rst b/Misc/NEWS.d/next/IDLE/2021-10-03-21-55-34.bpo-45357.etEExa.rst new file mode 100644 index 00000000000000..5f52ab0526014c --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2021-10-03-21-55-34.bpo-45357.etEExa.rst @@ -0,0 +1 @@ +Make idlelib.config.idleConf's functions GetExtensionKeys, __GetRawExtensionKeys, and GetKeyBinding look at user config files, allowing custom extensions to have key-binds defined in ~/.idlerc instead of in the default key-binds file. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy