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.
This commit is contained in:
Terry Jan Reedy 2016-06-26 22:05:10 -04:00
parent 754a5c1a1d
commit 68a53c5d39
6 changed files with 326 additions and 188 deletions

View File

@ -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)

View 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

View 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': {},

View File

@ -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)

View File

@ -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)

148
Lib/idlelib/query.py Normal file
View File

@ -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)