From 41d2abbd577171bc98b4cdf290520a2bbfdfe523 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 18 Sep 2026 00:03:15 +0300 Subject: [PATCH 1/5] gh-56596: Make IDLE key bindings work with Caps Lock on Tk does not fold the case of letter keysyms, and Caps Lock changes it, so bind each key sequence with the other case of its letters too. --- Lib/idlelib/editor.py | 10 ++++++++++ Lib/idlelib/idle_test/test_editor.py | 16 ++++++++++++++++ ...26-09-17-23-00-00.gh-issue-56596.capslock.rst | 1 + 3 files changed, 27 insertions(+) create mode 100644 Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 8e15319b5baab2..f2e87b0cbd710a 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -34,6 +34,10 @@ TK_TABWIDTH_DEFAULT = 8 darwin = sys.platform == 'darwin' +# A letter keysym in a key sequence, such as "s" in "". +_letter_key_re = re.compile(r'(?<=-Key-)[a-zA-Z](?=>)') + + class EditorWindow: is_shell = False # PyShell overrides. from idlelib.percolator import Percolator @@ -1186,6 +1190,12 @@ def apply_bindings(self, keydefs=None): for event, keylist in keydefs.items(): if keylist: text.event_add(event, *keylist) + # Caps Lock changes the case of letter keysyms, so bind + # the sequences with the other case too (gh-56596). + for keys in keylist: + other = _letter_key_re.sub(lambda m: m[0].swapcase(), keys) + if other != keys: + text.event_add(event, other) def fill_menus(self, menudefs=None, keydefs=None): """Fill in dropdown menus used by this window. diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index e32981091b72a6..a1034d7e8ebeef 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -31,6 +31,22 @@ def test_init(self): self.assertEqual(e.root, self.root) e._close() + def test_apply_bindings_caps_lock(self): + # gh-56596: Caps Lock changes the case of letter keysyms, so the + # sequences are bound with both cases. + e = Editor(root=self.root) + try: + e.apply_bindings({'<>': ['', ''], + '<>': ['']}) + self.assertEqual(set(e.text.event_info('<>')), + {'', '', + ''}) + self.assertEqual(set(e.text.event_info('<>')), + {'', + ''}) + finally: + e._close() + def test_set_width_zero_char_width(self): # A zero-width '0' must not raise ZeroDivisionError (gh-90304). e = Editor(root=self.root) diff --git a/Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst b/Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst new file mode 100644 index 00000000000000..517f82e7b139c9 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst @@ -0,0 +1 @@ +IDLE keyboard shortcuts with letters now work when Caps Lock is on. From 981f947ac8e8464b02e137289f60dcdd4b1989de Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Thu, 17 Sep 2026 23:25:46 -0400 Subject: [PATCH 2/5] Apply suggestion from @terryjreedy --- .../next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst b/Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst index 517f82e7b139c9..1f88d6366d5f13 100644 --- a/Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst +++ b/Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst @@ -1 +1,2 @@ -IDLE keyboard shortcuts with letters now work when Caps Lock is on. +Make IDLE keyboard shortcuts with ascii letters work when Caps Lock is on even when only one +of the upper and lowercase versions are defined. From 277857f0cf90401c50eff807c322dab18c914452 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 18 Sep 2026 00:04:30 -0400 Subject: [PATCH 3/5] Update Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst --- .../next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst b/Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst index 1f88d6366d5f13..70878e4f801263 100644 --- a/Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst +++ b/Misc/NEWS.d/next/IDLE/2026-09-17-23-00-00.gh-issue-56596.capslock.rst @@ -1,2 +1,2 @@ Make IDLE keyboard shortcuts with ascii letters work when Caps Lock is on even when only one -of the upper and lowercase versions are defined. +of the upper and lowercase versions are defined. From 3cf65a59c91b4e61ea0142e8039391020ffceb19 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 18 Sep 2026 10:15:11 +0300 Subject: [PATCH 4/5] Apply batched suggestions from code review Co-authored-by: Terry Jan Reedy --- Lib/idlelib/editor.py | 2 +- Lib/idlelib/idle_test/test_editor.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index f2e87b0cbd710a..1d5e181f42f764 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1194,7 +1194,7 @@ def apply_bindings(self, keydefs=None): # the sequences with the other case too (gh-56596). for keys in keylist: other = _letter_key_re.sub(lambda m: m[0].swapcase(), keys) - if other != keys: + if other not in keylist: text.event_add(event, other) def fill_menus(self, menudefs=None, keydefs=None): diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index a1034d7e8ebeef..ebcf683ac3a156 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -36,8 +36,8 @@ def test_apply_bindings_caps_lock(self): # sequences are bound with both cases. e = Editor(root=self.root) try: - e.apply_bindings({'<>': ['', ''], - '<>': ['']}) + e.apply_bindings({'<>': ('', ''), + '<>': ('',)}) self.assertEqual(set(e.text.event_info('<>')), {'', '', ''}) From d6e2147f1faa941c55ee14a2aee04a3d88fc1acd Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 18 Sep 2026 10:17:36 +0300 Subject: [PATCH 5/5] Test that existing case variants are not duplicated --- Lib/idlelib/idle_test/test_editor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index ebcf683ac3a156..2aaeb5037901b8 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -37,10 +37,14 @@ def test_apply_bindings_caps_lock(self): e = Editor(root=self.root) try: e.apply_bindings({'<>': ('', ''), - '<>': ('',)}) + '<>': ('',), + '<>': ('', '')}) self.assertEqual(set(e.text.event_info('<>')), {'', '', ''}) + # Existing variants are not added again. + self.assertEqual(e.text.event_info('<>'), + ('', '')) self.assertEqual(set(e.text.event_info('<>')), {'', ''})