https://github.com/python/cpython/commit/59c249ea21c5663911c3fc407a358d862f42571c
commit: 59c249ea21c5663911c3fc407a358d862f42571c
branch: 3.13
author: Terry Jan Reedy <[email protected]>
committer: terryjreedy <[email protected]>
date: 2026-09-27T02:24:48Z
summary:

[3.13] gh-55646: Do not crash IDLE on an invalid key binding (GH-152747) 
(#158265)

A typo in a key binding, such as <Alt-Key-up> for <Alt-Key-Up>, crashed IDLE
at startup.  It is now ignored with a warning.

On macOS an invalid binding such as <Alt-Key-up> is not parsed into a
MultiCall triplet, so it falls back to Tk's event_add(), which was left
unguarded and still crashed.  Guard the fallback too, and test both the
parsed (bind()) and unparsed (event_add()) paths on every platform.

---------
(cherry picked from commit b9546455ab0f65af65b0b69e86e7a2f65d287c16)

Co-authored-by: Serhiy Storchaka <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>

files:
A Misc/NEWS.d/next/IDLE/2026-07-01-21-00-00.gh-issue-55646.kBind9.rst
M Lib/idlelib/idle_test/test_multicall.py
M Lib/idlelib/multicall.py

diff --git a/Lib/idlelib/idle_test/test_multicall.py 
b/Lib/idlelib/idle_test/test_multicall.py
index 306c016c600cfe..4abf38877bd642 100644
--- a/Lib/idlelib/idle_test/test_multicall.py
+++ b/Lib/idlelib/idle_test/test_multicall.py
@@ -2,7 +2,7 @@
 
 from idlelib import multicall
 import unittest
-from test.support import requires
+from test.support import requires, captured_stderr
 from test.support.testcase import ExtraAssertions
 from tkinter import Tk, Text
 
@@ -44,6 +44,36 @@ def test_yview(self):
         mctext = self.mc(self.root)
         self.assertIs(mctext.yview.__func__, Text.yview)
 
+    def test_valid_binding(self):
+        # A valid key binding must bind without a warning (cf. gh-55646).
+        mctext = self.mc(self.root)
+        with captured_stderr() as stderr:
+            mctext.event_add('<<test-good>>', '<Control-Key-Up>')
+            mctext.bind('<<test-good>>', lambda e: None)
+        self.assertEqual(stderr.getvalue(), '')
+
+    def test_invalid_triplet_binding(self):
+        # gh-55646: '<Control-Key-up>' parses into a triplet on every platform,
+        # so Tk rejects it in bind().
+        mctext = self.mc(self.root)
+        with captured_stderr() as stderr:
+            mctext.event_add('<<test-bad>>', '<Control-Key-up>')  # Must not 
raise.
+            mctext.bind('<<test-bad>>', lambda e: None)  # Must not raise.
+        warning = stderr.getvalue()
+        self.assertIn('invalid key binding', warning)
+        self.assertIn('test-bad', warning)  # The offending action is named.
+
+    def test_invalid_nontriplet_binding(self):
+        # gh-55646: '<Foo-Key-Up>' has no valid modifier, so MultiCall does not
+        # parse it and falls back to Tk's event_add, which rejects it.
+        mctext = self.mc(self.root)
+        with captured_stderr() as stderr:
+            mctext.event_add('<<test-bad>>', '<Foo-Key-Up>')  # Must not raise.
+            mctext.bind('<<test-bad>>', lambda e: None)  # Must not raise.
+        warning = stderr.getvalue()
+        self.assertIn('invalid key binding', warning)
+        self.assertIn('test-bad', warning)  # The offending action is named.
+
     def test_event_delete_unbound_sequence(self):
         # gh-89360: deleting a sequence that was not added to a virtual
         # event is ignored instead of raising ValueError.
diff --git a/Lib/idlelib/multicall.py b/Lib/idlelib/multicall.py
index 95f4fee7fbe740..5589884e2af78f 100644
--- a/Lib/idlelib/multicall.py
+++ b/Lib/idlelib/multicall.py
@@ -310,6 +310,17 @@ def _triplet_to_sequence(triplet):
     else:
         return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>'
 
+
+def warn_bad_binding(virtual, sequence, err):
+    # gh-55646: warn instead of crashing on an invalid key binding.
+    action = virtual[2:-2] if virtual[:2] == '<<' and virtual[-2:] == '>>' \
+             else virtual
+    print(f'Warning: ignoring invalid key binding {sequence!r} '
+          f'for {action!r}: {err}. '
+          f'Please reconfigure it in the IDLE Settings dialog.',
+          file=sys.stderr)
+
+
 _multicall_dict = {}
 def MultiCallCreator(widget):
     """Return a MultiCall class which inherits its methods from the
@@ -343,8 +354,17 @@ def bind(self, sequence=None, func=None, add=None):
                             self.__binders[triplet[1]].unbind(triplet, ei[0])
                     ei[0] = func
                     if ei[0] is not None:
+                        bad = []
                         for triplet in ei[1]:
-                            self.__binders[triplet[1]].bind(triplet, func)
+                            try:
+                                self.__binders[triplet[1]].bind(triplet, func)
+                            except tkinter.TclError as err:
+                                warn_bad_binding(sequence,
+                                                 _triplet_to_sequence(triplet),
+                                                 err)
+                                bad.append(triplet)
+                        for triplet in bad:  # Drop the invalid sequences.
+                            ei[1].remove(triplet)
                 else:
                     self.__eventinfo[sequence] = [func, []]
             return widget.bind(self, sequence, func, add)
@@ -371,10 +391,19 @@ def event_add(self, virtual, *sequences):
                 triplet = _parse_sequence(seq)
                 if triplet is None:
                     #print("Tkinter event_add(%s)" % seq, file=sys.__stderr__)
-                    widget.event_add(self, virtual, seq)
+                    try:
+                        widget.event_add(self, virtual, seq)
+                    except tkinter.TclError as err:
+                        warn_bad_binding(virtual, seq, err)
+                        continue  # Drop the invalid sequence.
                 else:
                     if func is not None:
-                        self.__binders[triplet[1]].bind(triplet, func)
+                        try:
+                            self.__binders[triplet[1]].bind(triplet, func)
+                        except tkinter.TclError as err:
+                            warn_bad_binding(virtual,
+                                             _triplet_to_sequence(triplet), 
err)
+                            continue  # Drop the invalid sequence.
                     triplets.append(triplet)
 
         def event_delete(self, virtual, *sequences):
diff --git 
a/Misc/NEWS.d/next/IDLE/2026-07-01-21-00-00.gh-issue-55646.kBind9.rst 
b/Misc/NEWS.d/next/IDLE/2026-07-01-21-00-00.gh-issue-55646.kBind9.rst
new file mode 100644
index 00000000000000..31e97b3c64bcfe
--- /dev/null
+++ b/Misc/NEWS.d/next/IDLE/2026-07-01-21-00-00.gh-issue-55646.kBind9.rst
@@ -0,0 +1,3 @@
+Fix IDLE crash at startup when the user configuration contains an invalid key
+binding, such as ``<Alt-Key-up>`` instead of ``<Alt-Key-Up>``.  The invalid
+binding is now ignored with a warning.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to