]> granicus.if.org Git - python/commitdiff
Issue #27380: IDLE: add base Query dialog, with ttk widgets and subclass
authorTerry Jan Reedy <tjreedy@udel.edu>
Mon, 27 Jun 2016 02:05:10 +0000 (22:05 -0400)
committerTerry Jan Reedy <tjreedy@udel.edu>
Mon, 27 Jun 2016 02:05:10 +0000 (22:05 -0400)
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 [deleted file]
Lib/idlelib/configdialog.py
Lib/idlelib/idle_test/htest.py
Lib/idlelib/idle_test/test_config_sec.py [deleted file]
Lib/idlelib/idle_test/test_query.py [new file with mode: 0644]
Lib/idlelib/query.py [new file with mode: 0644]

diff --git a/Lib/idlelib/config_sec.py b/Lib/idlelib/config_sec.py
deleted file mode 100644 (file)
index 7b59124..0000000
+++ /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)
index f57c9a1adf3faf3a496c800eb3d562dff9b5a45c..6629d70ec6d68468e11c679431b33fea0673d1e3 100644 (file)
@@ -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
 
index 701f4d9fe638acc4d2cc010141c70905b51e92d5..d809d30dd0349df4f701675255e35d84cc718efe 100644 (file)
@@ -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, <nothing>, "
-           "'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 <Return> 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 (file)
index a98b484..0000000
+++ /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 (file)
index 0000000..e9c4bd4
--- /dev/null
@@ -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 (file)
index 0000000..e3937a1
--- /dev/null
@@ -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 <Return> 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('<Key-Return>', 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 <Return>) 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)