Skip to content

Commit 8c78aa7

Browse files
csabellaterryjreedy
authored andcommitted
bpo-6739: IDLE: Check for valid keybinding in config_keys (#2377)
Verify user-entered key sequences by trying to bind them with tk. Add tests for all 3 validation functions. Original patch by G Polo. Tests added by Cheryl Sabella.
1 parent af5392f commit 8c78aa7

File tree

3 files changed

+118
-26
lines changed

3 files changed

+118
-26
lines changed

Lib/idlelib/config_key.py

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
"""
44
from tkinter import *
55
from tkinter.ttk import Scrollbar
6-
import tkinter.messagebox as tkMessageBox
6+
from tkinter import messagebox
77
import string
88
import sys
99

10+
1011
class GetKeysDialog(Toplevel):
12+
13+
# Dialog title for invalid key sequence
14+
keyerror_title = 'Key Sequence Error'
15+
1116
def __init__(self, parent, title, action, currentKeySequences,
1217
_htest=False, _utest=False):
1318
"""
@@ -54,6 +59,10 @@ def __init__(self, parent, title, action, currentKeySequences,
5459
self.deiconify() #geometry set, unhide
5560
self.wait_window()
5661

62+
def showerror(self, *args, **kwargs):
63+
# Make testing easier. Replace in #30751.
64+
messagebox.showerror(*args, **kwargs)
65+
5766
def CreateWidgets(self):
5867
frameMain = Frame(self,borderwidth=2,relief=SUNKEN)
5968
frameMain.pack(side=TOP,expand=TRUE,fill=BOTH)
@@ -219,53 +228,70 @@ def TranslateKey(self, key, modifiers):
219228
return key
220229

221230
def OK(self, event=None):
222-
if self.advanced or self.KeysOK(): # doesn't check advanced string yet
223-
self.result=self.keyString.get()
224-
self.destroy()
231+
keys = self.keyString.get().strip()
232+
if not keys:
233+
self.showerror(title=self.keyerror_title, parent=self,
234+
message="No key specified.")
235+
return
236+
if (self.advanced or self.KeysOK(keys)) and self.bind_ok(keys):
237+
self.result = keys
238+
self.destroy()
225239

226240
def Cancel(self, event=None):
227241
self.result=''
228242
self.destroy()
229243

230-
def KeysOK(self):
244+
def KeysOK(self, keys):
231245
'''Validity check on user's 'basic' keybinding selection.
232246
233247
Doesn't check the string produced by the advanced dialog because
234248
'modifiers' isn't set.
235249
236250
'''
237-
keys = self.keyString.get()
238-
keys.strip()
239251
finalKey = self.listKeysFinal.get(ANCHOR)
240252
modifiers = self.GetModifiers()
241253
# create a key sequence list for overlap check:
242254
keySequence = keys.split()
243255
keysOK = False
244-
title = 'Key Sequence Error'
245-
if not keys:
246-
tkMessageBox.showerror(title=title, parent=self,
247-
message='No keys specified.')
248-
elif not keys.endswith('>'):
249-
tkMessageBox.showerror(title=title, parent=self,
250-
message='Missing the final Key')
256+
title = self.keyerror_title
257+
if not keys.endswith('>'):
258+
self.showerror(title, parent=self,
259+
message='Missing the final Key')
251260
elif (not modifiers
252261
and finalKey not in self.functionKeys + self.moveKeys):
253-
tkMessageBox.showerror(title=title, parent=self,
254-
message='No modifier key(s) specified.')
262+
self.showerror(title=title, parent=self,
263+
message='No modifier key(s) specified.')
255264
elif (modifiers == ['Shift']) \
256265
and (finalKey not in
257266
self.functionKeys + self.moveKeys + ('Tab', 'Space')):
258267
msg = 'The shift modifier by itself may not be used with'\
259268
' this key symbol.'
260-
tkMessageBox.showerror(title=title, parent=self, message=msg)
269+
self.showerror(title=title, parent=self, message=msg)
261270
elif keySequence in self.currentKeySequences:
262271
msg = 'This key combination is already in use.'
263-
tkMessageBox.showerror(title=title, parent=self, message=msg)
272+
self.showerror(title=title, parent=self, message=msg)
264273
else:
265274
keysOK = True
266275
return keysOK
267276

277+
def bind_ok(self, keys):
278+
"Return True if Tcl accepts the new keys else show message."
279+
280+
try:
281+
binding = self.bind(keys, lambda: None)
282+
except TclError as err:
283+
self.showerror(
284+
title=self.keyerror_title, parent=self,
285+
message=(f'The entered key sequence is not accepted.\n\n'
286+
f'Error: {err}'))
287+
return False
288+
else:
289+
self.unbind(keys, binding)
290+
return True
291+
268292

269293
if __name__ == '__main__':
294+
import unittest
295+
unittest.main('idlelib.idle_test.test_config_key', verbosity=2, exit=False)
270296
from idlelib.idle_test.htest import run
271297
run(GetKeysDialog)

Lib/idlelib/idle_test/test_config_key.py

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,92 @@
44
'''
55
from idlelib import config_key
66
from test.support import requires
7-
requires('gui')
7+
import sys
88
import unittest
99
from tkinter import Tk
10+
from idlelib.idle_test.mock_idle import Func
11+
from idlelib.idle_test.mock_tk import Var, Mbox_func
1012

1113

12-
class GetKeysTest(unittest.TestCase):
14+
class ValidationTest(unittest.TestCase):
15+
"Test validation methods: OK, KeysOK, bind_ok."
16+
17+
class Validator(config_key.GetKeysDialog):
18+
def __init__(self, *args, **kwargs):
19+
config_key.GetKeysDialog.__init__(self, *args, **kwargs)
20+
class listKeysFinal:
21+
get = Func()
22+
self.listKeysFinal = listKeysFinal
23+
GetModifiers = Func()
24+
showerror = Mbox_func()
1325

1426
@classmethod
1527
def setUpClass(cls):
28+
requires('gui')
1629
cls.root = Tk()
1730
cls.root.withdraw()
31+
cls.dialog = cls.Validator(
32+
cls.root, 'Title', '<<Test>>', [['<Key-F12>']], _utest=True)
1833

1934
@classmethod
2035
def tearDownClass(cls):
21-
cls.root.update() # Stop "can't run event command" warning.
36+
cls.dialog.Cancel()
37+
cls.root.update_idletasks()
2238
cls.root.destroy()
23-
del cls.root
39+
del cls.dialog, cls.root
40+
41+
def setUp(self):
42+
self.dialog.showerror.message = ''
43+
# A test that needs a particular final key value should set it.
44+
# A test that sets a non-blank modifier list should reset it to [].
45+
46+
def test_ok_empty(self):
47+
self.dialog.keyString.set(' ')
48+
self.dialog.OK()
49+
self.assertEqual(self.dialog.result, '')
50+
self.assertEqual(self.dialog.showerror.message, 'No key specified.')
51+
52+
def test_ok_good(self):
53+
self.dialog.keyString.set('<Key-F11>')
54+
self.dialog.listKeysFinal.get.result = 'F11'
55+
self.dialog.OK()
56+
self.assertEqual(self.dialog.result, '<Key-F11>')
57+
self.assertEqual(self.dialog.showerror.message, '')
58+
59+
def test_keys_no_ending(self):
60+
self.assertFalse(self.dialog.KeysOK('<Control-Shift'))
61+
self.assertIn('Missing the final', self.dialog.showerror.message)
62+
63+
def test_keys_no_modifier_bad(self):
64+
self.dialog.listKeysFinal.get.result = 'A'
65+
self.assertFalse(self.dialog.KeysOK('<Key-A>'))
66+
self.assertIn('No modifier', self.dialog.showerror.message)
67+
68+
def test_keys_no_modifier_ok(self):
69+
self.dialog.listKeysFinal.get.result = 'F11'
70+
self.assertTrue(self.dialog.KeysOK('<Key-F11>'))
71+
self.assertEqual(self.dialog.showerror.message, '')
72+
73+
def test_keys_shift_bad(self):
74+
self.dialog.listKeysFinal.get.result = 'a'
75+
self.dialog.GetModifiers.result = ['Shift']
76+
self.assertFalse(self.dialog.KeysOK('<a>'))
77+
self.assertIn('shift modifier', self.dialog.showerror.message)
78+
self.dialog.GetModifiers.result = []
79+
80+
def test_keys_dup(self):
81+
self.dialog.listKeysFinal.get.result = 'F12'
82+
self.dialog.GetModifiers.result = []
83+
self.assertFalse(self.dialog.KeysOK('<Key-F12>'))
84+
self.assertIn('already in use', self.dialog.showerror.message)
2485

86+
def test_bind_ok(self):
87+
self.assertTrue(self.dialog.bind_ok('<Control-Shift-Key-a>'))
88+
self.assertEqual(self.dialog.showerror.message, '')
2589

26-
def test_init(self):
27-
dia = config_key.GetKeysDialog(
28-
self.root, 'test', '<<Test>>', ['<Key-F12>'], _utest=True)
29-
dia.Cancel()
90+
def test_bind_not_ok(self):
91+
self.assertFalse(self.dialog.bind_ok('<Control-Shift>'))
92+
self.assertIn('not accepted', self.dialog.showerror.message)
3093

3194

3295
if __name__ == '__main__':
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
IDLE: Verify user-entered key sequences by trying to bind them with tk. Add
2+
tests for all 3 validation functions. Original patch by G Polo. Tests added
3+
by Cheryl Sabella.

0 commit comments

Comments
 (0)