Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 b954645)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
  • Loading branch information
3 people authored and miss-islington committed Sep 25, 2026
commit 691f96bef74205a16697a7af112de57242b3142f
32 changes: 31 additions & 1 deletion Lib/idlelib/idle_test/test_multicall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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.
Expand Down
35 changes: 32 additions & 3 deletions Lib/idlelib/multicall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading