From e7ab8913244e7b08a88102c865f71808badba048 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 1 Jul 2026 10:39:00 +0300 Subject: [PATCH 1/4] gh-55646: Do not crash IDLE on an invalid key binding A typo in a key binding, such as for , crashed IDLE at startup. It is now ignored with a warning. --- Lib/idlelib/idle_test/test_multicall.py | 11 +++++++++ Lib/idlelib/multicall.py | 23 +++++++++++++++++-- ...6-07-01-21-00-00.gh-issue-55646.kBind9.rst | 3 +++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/IDLE/2026-07-01-21-00-00.gh-issue-55646.kBind9.rst diff --git a/Lib/idlelib/idle_test/test_multicall.py b/Lib/idlelib/idle_test/test_multicall.py index 7d73761cfdfee82..b5ae8b24d869c20 100644 --- a/Lib/idlelib/idle_test/test_multicall.py +++ b/Lib/idlelib/idle_test/test_multicall.py @@ -1,9 +1,11 @@ "Test multicall, coverage 33%." from idlelib import multicall +import io import unittest from test.support import requires from tkinter import Tk, Text +from unittest import mock class MultiCallTest(unittest.TestCase): @@ -43,6 +45,15 @@ def test_yview(self): mctext = self.mc(self.root) self.assertIs(mctext.yview.__func__, Text.yview) + def test_invalid_binding(self): + # gh-55646: an invalid key binding must not crash IDLE. + mctext = self.mc(self.root) + # 'up' is not a keysym; it should be 'Up'. + mctext.event_add('<>', '') + with mock.patch('sys.stderr', new_callable=io.StringIO) as stderr: + mctext.bind('<>', lambda e: None) # Must not raise. + self.assertIn('invalid key binding', stderr.getvalue()) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Lib/idlelib/multicall.py b/Lib/idlelib/multicall.py index 41f818131130621..745c098d0d5e41d 100644 --- a/Lib/idlelib/multicall.py +++ b/Lib/idlelib/multicall.py @@ -310,6 +310,14 @@ def _triplet_to_sequence(triplet): else: return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>' + +def _warn_bad_binding(triplet, err): + # Ignore a key binding invalidated by a typo in the user's config + # instead of crashing IDLE (gh-55646). + print(f'Warning: ignoring invalid key binding ' + f'{_triplet_to_sequence(triplet)!r}: {err}', file=sys.stderr) + + _multicall_dict = {} def MultiCallCreator(widget): """Return a MultiCall class which inherits its methods from the @@ -343,8 +351,15 @@ 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(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) @@ -374,7 +389,11 @@ def event_add(self, virtual, *sequences): widget.event_add(self, virtual, seq) 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(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 000000000000000..31e97b3c64bcfee --- /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 ```` instead of ````. The invalid +binding is now ignored with a warning. From fd08181770b0d2e1faed1ef54f1038efff885eb9 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 1 Jul 2026 18:07:01 +0300 Subject: [PATCH 2/4] gh-55646: Fix the invalid key binding test on macOS On macOS an invalid binding such as 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. --- Lib/idlelib/idle_test/test_multicall.py | 31 ++++++++++++++++++------- Lib/idlelib/multicall.py | 20 +++++++++------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/Lib/idlelib/idle_test/test_multicall.py b/Lib/idlelib/idle_test/test_multicall.py index b5ae8b24d869c20..5c525a1d3590d14 100644 --- a/Lib/idlelib/idle_test/test_multicall.py +++ b/Lib/idlelib/idle_test/test_multicall.py @@ -1,11 +1,9 @@ "Test multicall, coverage 33%." from idlelib import multicall -import io import unittest -from test.support import requires +from test.support import requires, captured_stderr from tkinter import Tk, Text -from unittest import mock class MultiCallTest(unittest.TestCase): @@ -45,12 +43,29 @@ def test_yview(self): mctext = self.mc(self.root) self.assertIs(mctext.yview.__func__, Text.yview) - def test_invalid_binding(self): - # gh-55646: an invalid key binding must not crash IDLE. + def test_valid_binding(self): + # A valid key binding must bind without a warning (cf. gh-55646). mctext = self.mc(self.root) - # 'up' is not a keysym; it should be 'Up'. - mctext.event_add('<>', '') - with mock.patch('sys.stderr', new_callable=io.StringIO) as stderr: + with captured_stderr() as stderr: + mctext.event_add('<>', '') + mctext.bind('<>', lambda e: None) + self.assertEqual(stderr.getvalue(), '') + + def test_invalid_triplet_binding(self): + # gh-55646: '' 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('<>', '') # Must not raise. + mctext.bind('<>', lambda e: None) # Must not raise. + self.assertIn('invalid key binding', stderr.getvalue()) + + def test_invalid_nontriplet_binding(self): + # gh-55646: '' 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('<>', '') # Must not raise. mctext.bind('<>', lambda e: None) # Must not raise. self.assertIn('invalid key binding', stderr.getvalue()) diff --git a/Lib/idlelib/multicall.py b/Lib/idlelib/multicall.py index 745c098d0d5e41d..67861975f2ce82e 100644 --- a/Lib/idlelib/multicall.py +++ b/Lib/idlelib/multicall.py @@ -311,11 +311,10 @@ def _triplet_to_sequence(triplet): return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>' -def _warn_bad_binding(triplet, err): - # Ignore a key binding invalidated by a typo in the user's config - # instead of crashing IDLE (gh-55646). - print(f'Warning: ignoring invalid key binding ' - f'{_triplet_to_sequence(triplet)!r}: {err}', file=sys.stderr) +def _warn_bad_binding(sequence, err): + # gh-55646: warn instead of crashing on an invalid key binding. + print(f'Warning: ignoring invalid key binding {sequence!r}: {err}', + file=sys.stderr) _multicall_dict = {} @@ -356,7 +355,8 @@ def bind(self, sequence=None, func=None, add=None): try: self.__binders[triplet[1]].bind(triplet, func) except tkinter.TclError as err: - _warn_bad_binding(triplet, err) + _warn_bad_binding(_triplet_to_sequence(triplet), + err) bad.append(triplet) for triplet in bad: # Drop the invalid sequences. ei[1].remove(triplet) @@ -386,13 +386,17 @@ 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(seq, err) + continue # Drop the invalid sequence. else: if func is not None: try: self.__binders[triplet[1]].bind(triplet, func) except tkinter.TclError as err: - _warn_bad_binding(triplet, err) + _warn_bad_binding(_triplet_to_sequence(triplet), err) continue # Drop the invalid sequence. triplets.append(triplet) From 29471183275e19c80e6e11706afe99a90ed69ff4 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 2 Jul 2026 07:30:32 +0300 Subject: [PATCH 3/4] gh-55646: Name the action and suggest reconfiguring in the warning --- Lib/idlelib/idle_test/test_multicall.py | 9 +++++++-- Lib/idlelib/multicall.py | 16 +++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Lib/idlelib/idle_test/test_multicall.py b/Lib/idlelib/idle_test/test_multicall.py index 27248476a447955..db66fdf14ba2099 100644 --- a/Lib/idlelib/idle_test/test_multicall.py +++ b/Lib/idlelib/idle_test/test_multicall.py @@ -58,7 +58,9 @@ def test_invalid_triplet_binding(self): with captured_stderr() as stderr: mctext.event_add('<>', '') # Must not raise. mctext.bind('<>', lambda e: None) # Must not raise. - self.assertIn('invalid key binding', stderr.getvalue()) + 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: '' has no valid modifier, so MultiCall does not @@ -67,7 +69,10 @@ def test_invalid_nontriplet_binding(self): with captured_stderr() as stderr: mctext.event_add('<>', '') # Must not raise. mctext.bind('<>', lambda e: None) # Must not raise. - self.assertIn('invalid key binding', stderr.getvalue()) + 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 31dddd6fcc0d748..dbc2c2c594a4aa5 100644 --- a/Lib/idlelib/multicall.py +++ b/Lib/idlelib/multicall.py @@ -311,9 +311,13 @@ def _triplet_to_sequence(triplet): return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>' -def _warn_bad_binding(sequence, err): +def _warn_bad_binding(virtual, sequence, err): # gh-55646: warn instead of crashing on an invalid key binding. - print(f'Warning: ignoring invalid key binding {sequence!r}: {err}', + 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) @@ -355,7 +359,8 @@ def bind(self, sequence=None, func=None, add=None): try: self.__binders[triplet[1]].bind(triplet, func) except tkinter.TclError as err: - _warn_bad_binding(_triplet_to_sequence(triplet), + _warn_bad_binding(sequence, + _triplet_to_sequence(triplet), err) bad.append(triplet) for triplet in bad: # Drop the invalid sequences. @@ -389,14 +394,15 @@ def event_add(self, virtual, *sequences): try: widget.event_add(self, virtual, seq) except tkinter.TclError as err: - _warn_bad_binding(seq, err) + _warn_bad_binding(virtual, seq, err) continue # Drop the invalid sequence. else: if func is not None: try: self.__binders[triplet[1]].bind(triplet, func) except tkinter.TclError as err: - _warn_bad_binding(_triplet_to_sequence(triplet), err) + _warn_bad_binding(virtual, + _triplet_to_sequence(triplet), err) continue # Drop the invalid sequence. triplets.append(triplet) From ea8d8ada8a857663c8857912895ed7fd2755e0bd Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 2 Jul 2026 22:15:30 +0300 Subject: [PATCH 4/4] gh-55646: Rename warn_bad_binding to a non-underscored name idlelib is private, so its helpers need no leading underscore. Co-Authored-By: Claude Opus 4.8 --- Lib/idlelib/multicall.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/idlelib/multicall.py b/Lib/idlelib/multicall.py index dbc2c2c594a4aa5..5589884e2af78fb 100644 --- a/Lib/idlelib/multicall.py +++ b/Lib/idlelib/multicall.py @@ -311,7 +311,7 @@ def _triplet_to_sequence(triplet): return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>' -def _warn_bad_binding(virtual, sequence, err): +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 @@ -359,9 +359,9 @@ def bind(self, sequence=None, func=None, add=None): try: self.__binders[triplet[1]].bind(triplet, func) except tkinter.TclError as err: - _warn_bad_binding(sequence, - _triplet_to_sequence(triplet), - 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) @@ -394,15 +394,15 @@ def event_add(self, virtual, *sequences): try: widget.event_add(self, virtual, seq) except tkinter.TclError as err: - _warn_bad_binding(virtual, seq, err) + warn_bad_binding(virtual, seq, err) continue # Drop the invalid sequence. else: if func is not None: try: self.__binders[triplet[1]].bind(triplet, func) except tkinter.TclError as err: - _warn_bad_binding(virtual, - _triplet_to_sequence(triplet), err) + warn_bad_binding(virtual, + _triplet_to_sequence(triplet), err) continue # Drop the invalid sequence. triplets.append(triplet)