From 68a53c5d3964ae2f4658491822f83cf36510f39b Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Sun, 26 Jun 2016 22:05:10 -0400 Subject: [PATCH] Issue #27380: IDLE: add base Query dialog, with ttk widgets and subclass SectionName. These split class GetCfgSectionNameDialog from configSectionNameDialog.py, temporarily renamed config_sec.py in 3.7.9a2. More Query subclasses are planned. --- Lib/idlelib/config_sec.py | 98 -------------- Lib/idlelib/configdialog.py | 6 +- Lib/idlelib/idle_test/htest.py | 23 ++-- Lib/idlelib/idle_test/test_config_sec.py | 75 ----------- Lib/idlelib/idle_test/test_query.py | 164 +++++++++++++++++++++++ Lib/idlelib/query.py | 148 ++++++++++++++++++++ 6 files changed, 326 insertions(+), 188 deletions(-) delete mode 100644 Lib/idlelib/config_sec.py delete mode 100644 Lib/idlelib/idle_test/test_config_sec.py create mode 100644 Lib/idlelib/idle_test/test_query.py create mode 100644 Lib/idlelib/query.py diff --git a/Lib/idlelib/config_sec.py b/Lib/idlelib/config_sec.py deleted file mode 100644 index 7b59124507a..00000000000 --- a/Lib/idlelib/config_sec.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Dialog that allows user to specify a new config file section name. -Used to get new highlight theme and keybinding set names. -The 'return value' for the dialog, used two placed in configdialog.py, -is the .result attribute set in the Ok and Cancel methods. -""" -from tkinter import * -import tkinter.messagebox as tkMessageBox - -class GetCfgSectionNameDialog(Toplevel): - def __init__(self, parent, title, message, used_names, _htest=False): - """ - message - string, informational message to display - used_names - string collection, names already in use for validity check - _htest - bool, change box location when running htest - """ - Toplevel.__init__(self, parent) - self.configure(borderwidth=5) - self.resizable(height=FALSE, width=FALSE) - self.title(title) - self.transient(parent) - self.grab_set() - self.protocol("WM_DELETE_WINDOW", self.Cancel) - self.parent = parent - self.message = message - self.used_names = used_names - self.create_widgets() - self.withdraw() #hide while setting geometry - self.update_idletasks() - #needs to be done here so that the winfo_reqwidth is valid - self.messageInfo.config(width=self.frameMain.winfo_reqwidth()) - self.geometry( - "+%d+%d" % ( - parent.winfo_rootx() + - (parent.winfo_width()/2 - self.winfo_reqwidth()/2), - parent.winfo_rooty() + - ((parent.winfo_height()/2 - self.winfo_reqheight()/2) - if not _htest else 100) - ) ) #centre dialog over parent (or below htest box) - self.deiconify() #geometry set, unhide - self.wait_window() - - def create_widgets(self): - self.name = StringVar(self.parent) - self.fontSize = StringVar(self.parent) - self.frameMain = Frame(self, borderwidth=2, relief=SUNKEN) - self.frameMain.pack(side=TOP, expand=TRUE, fill=BOTH) - self.messageInfo = Message(self.frameMain, anchor=W, justify=LEFT, - padx=5, pady=5, text=self.message) #,aspect=200) - entryName = Entry(self.frameMain, textvariable=self.name, width=30) - entryName.focus_set() - self.messageInfo.pack(padx=5, pady=5) #, expand=TRUE, fill=BOTH) - entryName.pack(padx=5, pady=5) - - frameButtons = Frame(self, pady=2) - frameButtons.pack(side=BOTTOM) - self.buttonOk = Button(frameButtons, text='Ok', - width=8, command=self.Ok) - self.buttonOk.pack(side=LEFT, padx=5) - self.buttonCancel = Button(frameButtons, text='Cancel', - width=8, command=self.Cancel) - self.buttonCancel.pack(side=RIGHT, padx=5) - - def name_ok(self): - ''' After stripping entered name, check that it is a sensible - ConfigParser file section name. Return it if it is, '' if not. - ''' - name = self.name.get().strip() - if not name: #no name specified - tkMessageBox.showerror(title='Name Error', - message='No name specified.', parent=self) - elif len(name)>30: #name too long - tkMessageBox.showerror(title='Name Error', - message='Name too long. It should be no more than '+ - '30 characters.', parent=self) - name = '' - elif name in self.used_names: - tkMessageBox.showerror(title='Name Error', - message='This name is already in use.', parent=self) - name = '' - return name - - def Ok(self, event=None): - name = self.name_ok() - if name: - self.result = name - self.destroy() - - def Cancel(self, event=None): - self.result = '' - self.destroy() - -if __name__ == '__main__': - import unittest - unittest.main('idlelib.idle_test.test_config_name', verbosity=2, exit=False) - - from idlelib.idle_test.htest import run - run(GetCfgSectionNameDialog) diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index f57c9a1adf3..6629d70ec6d 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -18,7 +18,7 @@ import tkinter.font as tkFont from idlelib.config import idleConf from idlelib.dynoption import DynOptionMenu from idlelib.config_key import GetKeysDialog -from idlelib.config_sec import GetCfgSectionNameDialog +from idlelib.query import SectionName from idlelib.config_help import GetHelpSourceDialog from idlelib.tabbedpages import TabbedPageSet from idlelib.textview import view_text @@ -684,7 +684,7 @@ class ConfigDialog(Toplevel): def GetNewKeysName(self, message): usedNames = (idleConf.GetSectionList('user', 'keys') + idleConf.GetSectionList('default', 'keys')) - newKeySet = GetCfgSectionNameDialog( + newKeySet = SectionName( self, 'New Custom Key Set', message, usedNames).result return newKeySet @@ -837,7 +837,7 @@ class ConfigDialog(Toplevel): def GetNewThemeName(self, message): usedNames = (idleConf.GetSectionList('user', 'highlight') + idleConf.GetSectionList('default', 'highlight')) - newTheme = GetCfgSectionNameDialog( + newTheme = SectionName( self, 'New Custom Theme', message, usedNames).result return newTheme diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py index 701f4d9fe63..d809d30dd03 100644 --- a/Lib/idlelib/idle_test/htest.py +++ b/Lib/idlelib/idle_test/htest.py @@ -137,18 +137,6 @@ _editor_window_spec = { "Best to close editor first." } -GetCfgSectionNameDialog_spec = { - 'file': 'config_sec', - 'kwds': {'title':'Get Name', - 'message':'Enter something', - 'used_names': {'abc'}, - '_htest': True}, - 'msg': "After the text entered with [Ok] is stripped, , " - "'abc', or more that 30 chars are errors.\n" - "Close 'Get Name' with a valid entry (printed to Shell), " - "[Cancel], or [X]", - } - GetHelpSourceDialog_spec = { 'file': 'config_help', 'kwds': {'title': 'Get helpsource', @@ -245,6 +233,17 @@ _percolator_spec = { "Test for actions like text entry, and removal." } +Query_spec = { + 'file': 'query', + 'kwds': {'title':'Query', + 'message':'Enter something', + '_htest': True}, + 'msg': "Enter with or [Ok]. Print valid entry to Shell\n" + "Blank line, after stripping, is ignored\n" + "Close dialog with valid entry, [Cancel] or [X]", + } + + _replace_dialog_spec = { 'file': 'replace', 'kwds': {}, diff --git a/Lib/idlelib/idle_test/test_config_sec.py b/Lib/idlelib/idle_test/test_config_sec.py deleted file mode 100644 index a98b484bf7a..00000000000 --- a/Lib/idlelib/idle_test/test_config_sec.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Unit tests for idlelib.config_sec""" -import unittest -from idlelib.idle_test.mock_tk import Var, Mbox -from idlelib import config_sec as name_dialog_module - -name_dialog = name_dialog_module.GetCfgSectionNameDialog - -class Dummy_name_dialog: - # Mock for testing the following methods of name_dialog - name_ok = name_dialog.name_ok - Ok = name_dialog.Ok - Cancel = name_dialog.Cancel - # Attributes, constant or variable, needed for tests - used_names = ['used'] - name = Var() - result = None - destroyed = False - def destroy(self): - self.destroyed = True - -# name_ok calls Mbox.showerror if name is not ok -orig_mbox = name_dialog_module.tkMessageBox -showerror = Mbox.showerror - -class ConfigNameTest(unittest.TestCase): - dialog = Dummy_name_dialog() - - @classmethod - def setUpClass(cls): - name_dialog_module.tkMessageBox = Mbox - - @classmethod - def tearDownClass(cls): - name_dialog_module.tkMessageBox = orig_mbox - - def test_blank_name(self): - self.dialog.name.set(' ') - self.assertEqual(self.dialog.name_ok(), '') - self.assertEqual(showerror.title, 'Name Error') - self.assertIn('No', showerror.message) - - def test_used_name(self): - self.dialog.name.set('used') - self.assertEqual(self.dialog.name_ok(), '') - self.assertEqual(showerror.title, 'Name Error') - self.assertIn('use', showerror.message) - - def test_long_name(self): - self.dialog.name.set('good'*8) - self.assertEqual(self.dialog.name_ok(), '') - self.assertEqual(showerror.title, 'Name Error') - self.assertIn('too long', showerror.message) - - def test_good_name(self): - self.dialog.name.set(' good ') - showerror.title = 'No Error' # should not be called - self.assertEqual(self.dialog.name_ok(), 'good') - self.assertEqual(showerror.title, 'No Error') - - def test_ok(self): - self.dialog.destroyed = False - self.dialog.name.set('good') - self.dialog.Ok() - self.assertEqual(self.dialog.result, 'good') - self.assertTrue(self.dialog.destroyed) - - def test_cancel(self): - self.dialog.destroyed = False - self.dialog.Cancel() - self.assertEqual(self.dialog.result, '') - self.assertTrue(self.dialog.destroyed) - - -if __name__ == '__main__': - unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/idle_test/test_query.py b/Lib/idlelib/idle_test/test_query.py new file mode 100644 index 00000000000..e9c4bd40290 --- /dev/null +++ b/Lib/idlelib/idle_test/test_query.py @@ -0,0 +1,164 @@ +"""Test idlelib.query. + +Coverage: 100%. +""" +from test.support import requires +from tkinter import Tk +import unittest +from unittest import mock +from idlelib.idle_test.mock_tk import Var, Mbox_func +from idlelib import query +Query, SectionName = query.Query, query.SectionName + +class Dummy_Query: + # Mock for testing the following methods Query + entry_ok = Query.entry_ok + ok = Query.ok + cancel = Query.cancel + # Attributes, constant or variable, needed for tests + entry = Var() + result = None + destroyed = False + def destroy(self): + self.destroyed = True + +# entry_ok calls modal messagebox.showerror if entry is not ok. +# Mock showerrer returns, so don't need to click to continue. +orig_showerror = query.showerror +showerror = Mbox_func() # Instance has __call__ method. + +def setUpModule(): + query.showerror = showerror + +def tearDownModule(): + query.showerror = orig_showerror + + +class QueryTest(unittest.TestCase): + dialog = Dummy_Query() + + def setUp(self): + showerror.title = None + self.dialog.result = None + self.dialog.destroyed = False + + def test_blank_entry(self): + dialog = self.dialog + Equal = self.assertEqual + dialog.entry.set(' ') + Equal(dialog.entry_ok(), '') + Equal((dialog.result, dialog.destroyed), (None, False)) + Equal(showerror.title, 'Entry Error') + self.assertIn('Blank', showerror.message) + + def test_good_entry(self): + dialog = self.dialog + Equal = self.assertEqual + dialog.entry.set(' good ') + Equal(dialog.entry_ok(), 'good') + Equal((dialog.result, dialog.destroyed), (None, False)) + Equal(showerror.title, None) + + def test_ok(self): + dialog = self.dialog + Equal = self.assertEqual + dialog.entry.set('good') + Equal(dialog.ok(), None) + Equal((dialog.result, dialog.destroyed), ('good', True)) + + def test_cancel(self): + dialog = self.dialog + Equal = self.assertEqual + Equal(self.dialog.cancel(), None) + Equal((dialog.result, dialog.destroyed), (None, True)) + + +class Dummy_SectionName: + # Mock for testing the following method of Section_Name + entry_ok = SectionName.entry_ok + # Attributes, constant or variable, needed for tests + used_names = ['used'] + entry = Var() + +class SectionNameTest(unittest.TestCase): + dialog = Dummy_SectionName() + + + def setUp(self): + showerror.title = None + + def test_blank_name(self): + dialog = self.dialog + Equal = self.assertEqual + dialog.entry.set(' ') + Equal(dialog.entry_ok(), '') + Equal(showerror.title, 'Name Error') + self.assertIn('No', showerror.message) + + def test_used_name(self): + dialog = self.dialog + Equal = self.assertEqual + dialog.entry.set('used') + Equal(self.dialog.entry_ok(), '') + Equal(showerror.title, 'Name Error') + self.assertIn('use', showerror.message) + + def test_long_name(self): + dialog = self.dialog + Equal = self.assertEqual + dialog.entry.set('good'*8) + Equal(self.dialog.entry_ok(), '') + Equal(showerror.title, 'Name Error') + self.assertIn('too long', showerror.message) + + def test_good_entry(self): + dialog = self.dialog + Equal = self.assertEqual + dialog.entry.set(' good ') + Equal(dialog.entry_ok(), 'good') + Equal(showerror.title, None) + + +class QueryGuiTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.dialog = Query(cls.root, 'TEST', 'test', _utest=True) + cls.dialog.destroy = mock.Mock() + + @classmethod + def tearDownClass(cls): + del cls.dialog + cls.root.destroy() + del cls.root + + def setUp(self): + self.dialog.entry.delete(0, 'end') + self.dialog.result = None + self.dialog.destroy.reset_mock() + + def test_click_ok(self): + dialog = self.dialog + dialog.entry.insert(0, 'abc') + dialog.button_ok.invoke() + self.assertEqual(dialog.result, 'abc') + self.assertTrue(dialog.destroy.called) + + def test_click_blank(self): + dialog = self.dialog + dialog.button_ok.invoke() + self.assertEqual(dialog.result, None) + self.assertFalse(dialog.destroy.called) + + def test_click_cancel(self): + dialog = self.dialog + dialog.entry.insert(0, 'abc') + dialog.button_cancel.invoke() + self.assertEqual(dialog.result, None) + self.assertTrue(dialog.destroy.called) + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/query.py b/Lib/idlelib/query.py new file mode 100644 index 00000000000..e3937a1340b --- /dev/null +++ b/Lib/idlelib/query.py @@ -0,0 +1,148 @@ +""" +Dialogs that query users and verify the answer before accepting. +Use ttk widgets, limiting use to tcl/tk 8.5+, as in IDLE 3.6+. + +Query is the generic base class for a popup dialog. +The user must either enter a valid answer or close the dialog. +Entries are validated when is entered or [Ok] is clicked. +Entries are ignored when [Cancel] or [X] are clicked. +The 'return value' is .result set to either a valid answer or None. + +Subclass SectionName gets a name for a new config file section. +Configdialog uses it for new highlight theme and keybinding set names. +""" +# Query and Section name result from splitting GetCfgSectionNameDialog +# of configSectionNameDialog.py (temporarily config_sec.py) into +# generic and specific parts. + +from tkinter import FALSE, TRUE, Toplevel +from tkinter.messagebox import showerror +from tkinter.ttk import Frame, Button, Entry, Label + +class Query(Toplevel): + """Base class for getting verified answer from a user. + + For this base class, accept any non-blank string. + """ + def __init__(self, parent, title, message, + *, _htest=False, _utest=False): # Call from override. + """Create popup, do not return until tk widget destroyed. + + Additional subclass init must be done before calling this. + + title - string, title of popup dialog + message - string, informational message to display + _htest - bool, change box location when running htest + _utest - bool, leave window hidden and not modal + """ + Toplevel.__init__(self, parent) + self.configure(borderwidth=5) + self.resizable(height=FALSE, width=FALSE) + self.title(title) + self.transient(parent) + self.grab_set() + self.bind('', self.ok) + self.protocol("WM_DELETE_WINDOW", self.cancel) + self.parent = parent + self.message = message + self.create_widgets() + self.update_idletasks() + #needs to be done here so that the winfo_reqwidth is valid + self.withdraw() # Hide while configuring, especially geometry. + self.geometry( + "+%d+%d" % ( + parent.winfo_rootx() + + (parent.winfo_width()/2 - self.winfo_reqwidth()/2), + parent.winfo_rooty() + + ((parent.winfo_height()/2 - self.winfo_reqheight()/2) + if not _htest else 150) + ) ) #centre dialog over parent (or below htest box) + if not _utest: + self.deiconify() #geometry set, unhide + self.wait_window() + + def create_widgets(self): # Call from override, if any. + frame = Frame(self, borderwidth=2, relief='sunken', ) + label = Label(frame, anchor='w', justify='left', + text=self.message) + self.entry = Entry(frame, width=30) # Bind name for entry_ok. + self.entry.focus_set() + + buttons = Frame(self) # Bind buttons for invoke in unittest. + self.button_ok = Button(buttons, text='Ok', + width=8, command=self.ok) + self.button_cancel = Button(buttons, text='Cancel', + width=8, command=self.cancel) + + frame.pack(side='top', expand=TRUE, fill='both') + label.pack(padx=5, pady=5) + self.entry.pack(padx=5, pady=5) + buttons.pack(side='bottom') + self.button_ok.pack(side='left', padx=5) + self.button_cancel.pack(side='right', padx=5) + + def entry_ok(self): # Usually replace. + "Check that entry not blank." + entry = self.entry.get().strip() + if not entry: + showerror(title='Entry Error', + message='Blank line.', parent=self) + return entry + + def ok(self, event=None): # Do not replace. + '''If entry is valid, bind it to 'result' and destroy tk widget. + + Otherwise leave dialog open for user to correct entry or cancel. + ''' + entry = self.entry_ok() + if entry: + self.result = entry + self.destroy() + else: + # [Ok] (but not ) moves focus. Move it back. + self.entry.focus_set() + + def cancel(self, event=None): # Do not replace. + "Set dialog result to None and destroy tk widget." + self.result = None + self.destroy() + + +class SectionName(Query): + "Get a name for a config file section name." + + def __init__(self, parent, title, message, used_names, + *, _htest=False, _utest=False): + "used_names - collection of strings already in use" + + self.used_names = used_names + Query.__init__(self, parent, title, message, + _htest=_htest, _utest=_utest) + # This call does ot return until tk widget is destroyed. + + def entry_ok(self): + '''Stripping entered name, check that it is a sensible + ConfigParser file section name. Return it if it is, '' if not. + ''' + name = self.entry.get().strip() + if not name: + showerror(title='Name Error', + message='No name specified.', parent=self) + elif len(name)>30: + showerror(title='Name Error', + message='Name too long. It should be no more than '+ + '30 characters.', parent=self) + name = '' + elif name in self.used_names: + showerror(title='Name Error', + message='This name is already in use.', parent=self) + name = '' + return name + + +if __name__ == '__main__': + import unittest + unittest.main('idlelib.idle_test.test_query', verbosity=2, exit=False) + + from idlelib.idle_test.htest import run + run(Query)