[Python-checkins] cpython: Issue #27380: IDLE: add base Query dialog, with ttk widgets and subclass

terry.reedy python-checkins at python.org
Sun Jun 26 22:05:24 EDT 2016


https://hg.python.org/cpython/rev/4796d7fb00c5
changeset:   102188:4796d7fb00c5
user:        Terry Jan Reedy <tjreedy at udel.edu>
date:        Sun Jun 26 22:05:10 2016 -0400
summary:
  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.

files:
  Lib/idlelib/configdialog.py         |    6 +-
  Lib/idlelib/idle_test/htest.py      |   23 +-
  Lib/idlelib/idle_test/test_query.py |  193 +++++++++++----
  Lib/idlelib/query.py                |  160 ++++++++----
  4 files changed, 260 insertions(+), 122 deletions(-)


diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py
--- a/Lib/idlelib/configdialog.py
+++ b/Lib/idlelib/configdialog.py
@@ -18,7 +18,7 @@
 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 @@
     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 @@
     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
--- a/Lib/idlelib/idle_test/htest.py
+++ b/Lib/idlelib/idle_test/htest.py
@@ -137,18 +137,6 @@
            "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 @@
            "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_query.py
rename from Lib/idlelib/idle_test/test_config_sec.py
rename to Lib/idlelib/idle_test/test_query.py
--- a/Lib/idlelib/idle_test/test_config_sec.py
+++ b/Lib/idlelib/idle_test/test_query.py
@@ -1,74 +1,163 @@
-"""Unit tests for idlelib.config_sec"""
+"""Test idlelib.query.
+
+Coverage: 100%.
+"""
+from test.support import requires
+from tkinter import Tk
 import unittest
-from idlelib.idle_test.mock_tk import Var, Mbox
-from idlelib import config_sec as name_dialog_module
+from unittest import mock
+from idlelib.idle_test.mock_tk import Var, Mbox_func
+from idlelib import query
+Query, SectionName = query.Query, query.SectionName
 
-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
+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
-    used_names = ['used']
-    name = Var()
+    entry = 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
+# 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.
 
-class ConfigNameTest(unittest.TestCase):
-    dialog = Dummy_name_dialog()
+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):
-        name_dialog_module.tkMessageBox = Mbox
+        requires('gui')
+        cls.root = Tk()
+        cls.dialog = Query(cls.root, 'TEST', 'test', _utest=True)
+        cls.dialog.destroy = mock.Mock()
 
     @classmethod
     def tearDownClass(cls):
-        name_dialog_module.tkMessageBox = orig_mbox
+        del cls.dialog
+        cls.root.destroy()
+        del cls.root
 
-    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 setUp(self):
+        self.dialog.entry.delete(0, 'end')
+        self.dialog.result = None
+        self.dialog.destroy.reset_mock()
 
-    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_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_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_click_blank(self):
+        dialog = self.dialog
+        dialog.button_ok.invoke()
+        self.assertEqual(dialog.result, None)
+        self.assertFalse(dialog.destroy.called)
 
-    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)
+    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__':
diff --git a/Lib/idlelib/config_sec.py b/Lib/idlelib/query.py
rename from Lib/idlelib/config_sec.py
rename to Lib/idlelib/query.py
--- a/Lib/idlelib/config_sec.py
+++ b/Lib/idlelib/query.py
@@ -1,18 +1,39 @@
 """
-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.
+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.
 """
-from tkinter import *
-import tkinter.messagebox as tkMessageBox
+# Query and Section name result from splitting GetCfgSectionNameDialog
+# of configSectionNameDialog.py (temporarily config_sec.py) into
+# generic and specific parts.
 
-class GetCfgSectionNameDialog(Toplevel):
-    def __init__(self, parent, title, message, used_names, _htest=False):
-        """
+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
-        used_names - string collection, names already in use for validity check
         _htest - bool, change box location when running htest
+        _utest - bool, leave window hidden and not modal
         """
         Toplevel.__init__(self, parent)
         self.configure(borderwidth=5)
@@ -20,79 +41,108 @@
         self.title(title)
         self.transient(parent)
         self.grab_set()
-        self.protocol("WM_DELETE_WINDOW", self.Cancel)
+        self.bind('<Key-Return>', self.ok)
+        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.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 100)
+                    if not _htest else 150)
                 ) )  #centre dialog over parent (or below htest box)
-        self.deiconify()  #geometry set, unhide
-        self.wait_window()
+        if not _utest:
+            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)
+    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()
 
-        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)
+        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)
 
-    def name_ok(self):
-        ''' After stripping entered name, check that it is a  sensible
+        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.name.get().strip()
-        if not name: #no name specified
-            tkMessageBox.showerror(title='Name Error',
+        name = self.entry.get().strip()
+        if not name:
+            showerror(title='Name Error',
                     message='No name specified.', parent=self)
-        elif len(name)>30: #name too long
-            tkMessageBox.showerror(title='Name Error',
+        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:
-            tkMessageBox.showerror(title='Name Error',
+            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)
+    unittest.main('idlelib.idle_test.test_query', verbosity=2, exit=False)
 
     from idlelib.idle_test.htest import run
-    run(GetCfgSectionNameDialog)
+    run(Query)

-- 
Repository URL: https://hg.python.org/cpython


More information about the Python-checkins mailing list