https://github.com/python/cpython/commit/c180433f6dbf989683deb301a4256e10a90d42ee commit: c180433f6dbf989683deb301a4256e10a90d42ee branch: 3.14 author: Miss Islington (bot) <[email protected]> committer: terryjreedy <[email protected]> date: 2026-09-25T02:26:24Z summary:
[3.14] gh-55646: Do not crash IDLE on an invalid key binding (GH-152747) (#158150) gh-55646: Do not crash IDLE on an invalid key binding (GH-152747) 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: Terry Jan Reedy <[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 fcce2030579f4b..db66fdf14ba209 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 tkinter import Tk, Text @@ -43,6 +43,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]
