Skip to content

Commit 5f47a68

Browse files
committed
bpo-30780: IDLE: Complete keys and highlight coverage for configdialog and add tests for buttons.
1 parent a15d155 commit 5f47a68

2 files changed

Lines changed: 227 additions & 17 deletions

File tree

Lib/idlelib/configdialog.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,19 @@ def create_action_buttons(self):
139139
else:
140140
padding_args = {'padding': (6, 3)}
141141
outer = Frame(self, padding=2)
142-
buttons = Frame(outer, padding=2)
142+
buttons_frame = Frame(outer, padding=2)
143+
self.buttons = {}
143144
for txt, cmd in (
144145
('Ok', self.ok),
145146
('Apply', self.apply),
146147
('Cancel', self.cancel),
147148
('Help', self.help)):
148-
Button(buttons, text=txt, command=cmd, takefocus=FALSE,
149-
**padding_args).pack(side=LEFT, padx=5)
149+
self.buttons[txt] = Button(buttons_frame, text=txt, command=cmd,
150+
takefocus=FALSE, **padding_args)
151+
self.buttons[txt].pack(side=LEFT, padx=5)
150152
# Add space above buttons.
151153
Frame(outer, height=2, borderwidth=0).pack(side=TOP)
152-
buttons.pack(side=BOTTOM)
154+
buttons_frame.pack(side=BOTTOM)
153155
return outer
154156

155157
def ok(self):
@@ -193,8 +195,9 @@ def help(self):
193195
view_text: Method from textview module.
194196
"""
195197
page = self.note.tab(self.note.select(), option='text').strip()
196-
view_text(self, title='Help for IDLE preferences',
197-
text=help_common+help_pages.get(page, ''))
198+
self._current_viewtext = view_text(
199+
self, title='Help for IDLE preferences',
200+
text=help_common+help_pages.get(page, ''))
198201

199202
def deactivate_current_config(self):
200203
"""Remove current key bindings.
@@ -794,6 +797,8 @@ def create_page_highlight(self):
794797
frame_custom, relief=SOLID, borderwidth=1,
795798
font=('courier', 12, ''), cursor='hand2', width=21, height=13,
796799
takefocus=FALSE, highlightthickness=0, wrap=NONE)
800+
# Override to prevent double click and motion from propagating to
801+
# other handlers.
797802
text.bind('<Double-Button-1>', lambda e: 'break')
798803
text.bind('<B1-Motion>', lambda e: 'break')
799804
text_and_tags=(

Lib/idlelib/idle_test/test_configdialog.py

Lines changed: 216 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import unittest
1010
from unittest import mock
1111
from idlelib.idle_test.mock_idle import Func
12-
from tkinter import Tk, Frame, StringVar, IntVar, BooleanVar, DISABLED, NORMAL
12+
from tkinter import (Tk, Frame, StringVar, IntVar, BooleanVar, DISABLED,
13+
NORMAL, TclError)
1314
from idlelib import config
1415
from idlelib.configdialog import idleConf, changes, tracers
1516

@@ -31,13 +32,15 @@
3132
keyspage = changes['keys']
3233
extpage = changes['extensions']
3334

35+
3436
def setUpModule():
3537
global root, dialog
3638
idleConf.userCfg = testcfg
3739
root = Tk()
3840
# root.withdraw() # Comment out, see issue 30870
3941
dialog = configdialog.ConfigDialog(root, 'Test', _utest=True)
4042

43+
4144
def tearDownModule():
4245
global root, dialog
4346
idleConf.userCfg = usercfg
@@ -50,6 +53,66 @@ def tearDownModule():
5053
del root
5154

5255

56+
class ConfigDialogTest(unittest.TestCase):
57+
58+
def test_deactivate_current_config(self):
59+
pass
60+
61+
def activate_config_changes(self):
62+
pass
63+
64+
65+
class ConfigDialogGUITest(unittest.TestCase):
66+
67+
def test_click_ok(self):
68+
d = dialog
69+
apply = d.apply = mock.Mock()
70+
destroy = d.destroy = mock.Mock()
71+
d.buttons['Ok'].invoke()
72+
apply.assert_called_once()
73+
destroy.assert_called_once()
74+
del d.destroy, d.apply
75+
76+
def test_click_apply(self):
77+
d = dialog
78+
deactivate = d.deactivate_current_config = mock.Mock()
79+
save_ext = d.save_all_changed_extensions = mock.Mock()
80+
activate = d.activate_config_changes = mock.Mock()
81+
d.buttons['Apply'].invoke()
82+
deactivate.assert_called_once()
83+
save_ext.assert_called_once()
84+
activate.assert_called_once()
85+
del d.save_all_changed_extensions
86+
del d.activate_config_changes, d.deactivate_current_config
87+
88+
def test_click_cancel(self):
89+
d = dialog
90+
destroy = d.destroy = mock.Mock()
91+
d.buttons['Cancel'].invoke()
92+
destroy.assert_called_once()
93+
del d.destroy
94+
95+
@mock.patch.object(configdialog, 'view_text')
96+
def test_click_help(self, mock_textview):
97+
d = dialog
98+
99+
# Highlight help has extra text.
100+
d.note.select(dialog.highpage)
101+
d.buttons['Help'].invoke()
102+
mock_textview.assert_called_once()
103+
kwargs = mock_textview.call_args[1]
104+
self.assertEqual(kwargs['title'], 'Help for IDLE preferences')
105+
self.assertIn('Highlighting:', kwargs['text'])
106+
107+
# Keys help page extra text.
108+
d.note.select(dialog.keyspage)
109+
d.buttons['Help'].invoke()
110+
mock_textview.assert_called()
111+
kwargs = mock_textview.call_args[1]
112+
self.assertEqual(kwargs['title'], 'Help for IDLE preferences')
113+
self.assertIn('Keys:', kwargs['text'])
114+
115+
53116
class FontPageTest(unittest.TestCase):
54117
"""Test that font widgets enable users to make font changes.
55118
@@ -418,6 +481,90 @@ def click_it(start):
418481
eq(d.highlight_target.get(), elem[tag])
419482
eq(d.set_highlight_target.called, count)
420483

484+
def test_highlight_sample_double_click(self):
485+
# Test double click on highlight_sample.
486+
eq = self.assertEqual
487+
d = self.page
488+
489+
hs = d.highlight_sample
490+
hs.focus_force()
491+
hs.see(1.0)
492+
hs.update_idletasks()
493+
494+
# Test binding from configdialog.
495+
hs.event_generate('<Enter>', x=0, y=0)
496+
hs.event_generate('<Motion>', x=0, y=0)
497+
# Double click is a sequence of two clicks in a row.
498+
for _ in range(2):
499+
hs.event_generate('<ButtonPress-1>', x=0, y=0)
500+
hs.event_generate('<ButtonRelease-1>', x=0, y=0)
501+
502+
self.assertNotEqual(d.highlight_target.get(), 'text double')
503+
with self.assertRaises(TclError, msg='text doesn\'t contain any characters tagged with "sel"'):
504+
hs.get('sel.first', 'sel.last')
505+
506+
# Change binding on double click. This will allow the event to
507+
# propagate to the other levels. The default binding for double
508+
# click is to select the word that was double clicked.
509+
hs.bind('<Double-Button-1>', lambda e: d.highlight_target.set('test double'))
510+
511+
hs.event_generate('<Enter>', x=0, y=0)
512+
hs.event_generate('<Motion>', x=0, y=0)
513+
# Double click is a sequence of two clicks in a row.
514+
for _ in range(2):
515+
hs.event_generate('<ButtonPress-1>', x=0, y=0)
516+
hs.event_generate('<ButtonRelease-1>', x=0, y=0)
517+
518+
eq(d.highlight_target.get(), 'test double')
519+
eq(hs.get('sel.first', 'sel.last'), '\n')
520+
521+
# Remove selection and rebind.
522+
hs.tag_remove('sel', '0.0', 'end')
523+
hs.bind('<Double-Button-1>', lambda e: 'break')
524+
525+
def test_highlight_sample_b1_motion(self):
526+
# Test button motion on highlight_sample.
527+
eq = self.assertEqual
528+
d = self.page
529+
530+
hs = d.highlight_sample
531+
hs.focus_force()
532+
hs.see(1.0)
533+
hs.update_idletasks()
534+
535+
x, y, dx, dy, offset = hs.dlineinfo('1.0')
536+
537+
# Test binding from configdialog.
538+
hs.event_generate('<Leave>')
539+
hs.event_generate('<Enter>')
540+
hs.event_generate('<Motion>', x=x, y=y)
541+
hs.event_generate('<ButtonPress-1>', x=x, y=y)
542+
hs.event_generate('<B1-Motion>', x=dx, y=dy)
543+
hs.event_generate('<ButtonRelease-1>', x=dx, y=dy)
544+
545+
eq(hs.tag_ranges('sel'), ())
546+
547+
# Remove binding on button motion. This will allow the event to
548+
# propagate to the other levels. The default binding for button
549+
# motion is to select the text that was moused over while the button
550+
# is held.
551+
hs.unbind('<B1-Motion>')
552+
x, y, dx, dy, offset = hs.dlineinfo('4.0')
553+
554+
hs.event_generate('<Leave>')
555+
hs.event_generate('<Enter>')
556+
hs.event_generate('<Motion>', x=x, y=y)
557+
hs.event_generate('<ButtonPress-1>', x=x, y=y)
558+
hs.event_generate('<B1-Motion>', x=x+dx, y=y+dy)
559+
hs.event_generate('<ButtonRelease-1>', x=x+dx, y=y+dy)
560+
561+
self.assertNotEqual(hs.tag_ranges('sel'), ())
562+
self.assertIn('def', hs.get('sel.first', 'sel.last'))
563+
564+
# Remove selection and rebind.
565+
hs.tag_remove('sel', '0.0', 'end')
566+
hs.bind('<B1-Motion>', lambda e: 'break')
567+
421568
def test_set_theme_type(self):
422569
eq = self.assertEqual
423570
d = self.page
@@ -548,6 +695,17 @@ def test_create_new_and_save_new(self):
548695
self.assertFalse(d.theme_source.get()) # Use custom set.
549696
eq(d.set_theme_type.called, 1)
550697

698+
# Call with same name as first time to make sure it adds it only once.
699+
d.theme_source.set(True)
700+
d.builtin_name.set('IDLE Classic')
701+
d.create_new(first_new)
702+
eq(idleConf.GetSectionList('user', 'highlight'), [first_new])
703+
eq(idleConf.GetThemeDict('default', 'IDLE Classic'),
704+
idleConf.GetThemeDict('user', first_new))
705+
eq(d.custom_name.get(), first_new)
706+
self.assertFalse(d.theme_source.get()) # Use custom set.
707+
eq(d.set_theme_type.called, 2)
708+
551709
# Test that changed targets are in new theme.
552710
changes.add_option('highlight', first_new, 'hit-background', 'yellow')
553711
self.assertNotIn(second_new, idleConf.userCfg)
@@ -651,16 +809,21 @@ def test_delete_custom(self):
651809
idleConf.userCfg['highlight'].SetOption(theme_name, 'name', 'value')
652810
highpage[theme_name] = {'option': 'True'}
653811

812+
theme_name2 = 'other theme'
813+
idleConf.userCfg['highlight'].SetOption(theme_name2, 'name', 'value')
814+
highpage[theme_name2] = {'option': 'False'}
815+
654816
# Force custom theme.
655-
d.theme_source.set(False)
817+
d.custom_theme_on.state(('!disabled',))
818+
d.custom_theme_on.invoke()
656819
d.custom_name.set(theme_name)
657820

658821
# Cancel deletion.
659822
yesno.result = False
660823
d.button_delete_custom.invoke()
661824
eq(yesno.called, 1)
662825
eq(highpage[theme_name], {'option': 'True'})
663-
eq(idleConf.GetSectionList('user', 'highlight'), ['spam theme'])
826+
eq(idleConf.GetSectionList('user', 'highlight'), [theme_name, theme_name2])
664827
eq(dialog.deactivate_current_config.called, 0)
665828
eq(dialog.activate_config_changes.called, 0)
666829
eq(d.set_theme_type.called, 0)
@@ -670,13 +833,26 @@ def test_delete_custom(self):
670833
d.button_delete_custom.invoke()
671834
eq(yesno.called, 2)
672835
self.assertNotIn(theme_name, highpage)
673-
eq(idleConf.GetSectionList('user', 'highlight'), [])
674-
eq(d.custom_theme_on.state(), ('disabled',))
675-
eq(d.custom_name.get(), '- no custom themes -')
836+
eq(idleConf.GetSectionList('user', 'highlight'), [theme_name2])
837+
eq(d.custom_theme_on.state(), ())
838+
eq(d.custom_name.get(), theme_name2)
676839
eq(dialog.deactivate_current_config.called, 1)
677840
eq(dialog.activate_config_changes.called, 1)
678841
eq(d.set_theme_type.called, 1)
679842

843+
# Confirm deletion of second theme - empties list.
844+
d.custom_name.set(theme_name2)
845+
yesno.result = True
846+
d.button_delete_custom.invoke()
847+
eq(yesno.called, 3)
848+
self.assertNotIn(theme_name, highpage)
849+
eq(idleConf.GetSectionList('user', 'highlight'), [])
850+
eq(d.custom_theme_on.state(), ('disabled',))
851+
eq(d.custom_name.get(), '- no custom themes -')
852+
eq(dialog.deactivate_current_config.called, 2)
853+
eq(dialog.activate_config_changes.called, 2)
854+
eq(d.set_theme_type.called, 2)
855+
680856
del dialog.activate_config_changes, dialog.deactivate_current_config
681857
del d.askyesno
682858

@@ -977,6 +1153,17 @@ def test_create_new_key_set_and_save_new_key_set(self):
9771153
self.assertFalse(d.keyset_source.get()) # Use custom set.
9781154
eq(d.set_keys_type.called, 1)
9791155

1156+
# Call with same name as first time to make sure it adds it only once.
1157+
d.keyset_source.set(True)
1158+
d.builtin_name.set('IDLE Classic Windows')
1159+
d.create_new_key_set(first_new)
1160+
eq(idleConf.GetSectionList('user', 'keys'), [first_new])
1161+
eq(idleConf.GetKeySet('IDLE Classic Windows'),
1162+
idleConf.GetKeySet(first_new))
1163+
eq(d.custom_name.get(), first_new)
1164+
self.assertFalse(d.keyset_source.get()) # Use custom set.
1165+
eq(d.set_keys_type.called, 2)
1166+
9801167
# Test that changed keybindings are in new keyset.
9811168
changes.add_option('keys', first_new, 'copy', '<Key-F11>')
9821169
self.assertNotIn(second_new, idleConf.userCfg)
@@ -1043,16 +1230,21 @@ def test_delete_custom_keys(self):
10431230
idleConf.userCfg['keys'].SetOption(keyset_name, 'name', 'value')
10441231
keyspage[keyset_name] = {'option': 'True'}
10451232

1233+
keyset_name2 = 'other key set'
1234+
idleConf.userCfg['keys'].SetOption(keyset_name2, 'name', 'value')
1235+
keyspage[keyset_name2] = {'option': 'False'}
1236+
10461237
# Force custom keyset.
1047-
d.keyset_source.set(False)
1238+
d.custom_keyset_on.state(('!disabled',))
1239+
d.custom_keyset_on.invoke()
10481240
d.custom_name.set(keyset_name)
10491241

10501242
# Cancel deletion.
10511243
yesno.result = False
10521244
d.button_delete_custom_keys.invoke()
10531245
eq(yesno.called, 1)
10541246
eq(keyspage[keyset_name], {'option': 'True'})
1055-
eq(idleConf.GetSectionList('user', 'keys'), ['spam key set'])
1247+
eq(idleConf.GetSectionList('user', 'keys'), [keyset_name, keyset_name2])
10561248
eq(dialog.deactivate_current_config.called, 0)
10571249
eq(dialog.activate_config_changes.called, 0)
10581250
eq(d.set_keys_type.called, 0)
@@ -1062,13 +1254,26 @@ def test_delete_custom_keys(self):
10621254
d.button_delete_custom_keys.invoke()
10631255
eq(yesno.called, 2)
10641256
self.assertNotIn(keyset_name, keyspage)
1065-
eq(idleConf.GetSectionList('user', 'keys'), [])
1066-
eq(d.custom_keyset_on.state(), ('disabled',))
1067-
eq(d.custom_name.get(), '- no custom keys -')
1257+
eq(idleConf.GetSectionList('user', 'keys'), [keyset_name2])
1258+
eq(d.custom_keyset_on.state(), ())
1259+
eq(d.custom_name.get(), keyset_name2)
10681260
eq(dialog.deactivate_current_config.called, 1)
10691261
eq(dialog.activate_config_changes.called, 1)
10701262
eq(d.set_keys_type.called, 1)
10711263

1264+
# Confirm deletion of second keyset - empties list.
1265+
d.custom_name.set(keyset_name2)
1266+
yesno.result = True
1267+
d.button_delete_custom_keys.invoke()
1268+
eq(yesno.called, 3)
1269+
self.assertNotIn(keyset_name, keyspage)
1270+
eq(idleConf.GetSectionList('user', 'keys'), [])
1271+
eq(d.custom_keyset_on.state(), ('disabled',))
1272+
eq(d.custom_name.get(), '- no custom keys -')
1273+
eq(dialog.deactivate_current_config.called, 2)
1274+
eq(dialog.activate_config_changes.called, 2)
1275+
eq(d.set_keys_type.called, 2)
1276+
10721277
del dialog.activate_config_changes, dialog.deactivate_current_config
10731278
del d.askyesno
10741279

0 commit comments

Comments
 (0)