summaryrefslogtreecommitdiffstats
path: root/Lib/idlelib
diff options
context:
space:
mode:
authorTerry Jan Reedy <tjreedy@udel.edu>2016-06-27 02:05:10 (GMT)
committerTerry Jan Reedy <tjreedy@udel.edu>2016-06-27 02:05:10 (GMT)
commit68a53c5d3964ae2f4658491822f83cf36510f39b (patch)
tree6124e372ec80523d1718af3882c1b811febf67c5 /Lib/idlelib
parent754a5c1a1d8cf0b3d61fb4552c57350b0d849089 (diff)
downloadcpython-68a53c5d3964ae2f4658491822f83cf36510f39b.zip
cpython-68a53c5d3964ae2f4658491822f83cf36510f39b.tar.gz
cpython-68a53c5d3964ae2f4658491822f83cf36510f39b.tar.bz2
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.
Diffstat (limited to 'Lib/idlelib')
-rw-r--r--Lib/idlelib/config_sec.py98
-rw-r--r--Lib/idlelib/configdialog.py6
-rw-r--r--Lib/idlelib/idle_test/htest.py23
-rw-r--r--Lib/idlelib/idle_test/test_config_sec.py75
-rw-r--r--Lib/idlelib/idle_test/test_query.py164
-rw-r--r--Lib/idlelib/query.py148
6 files changed, 326 insertions, 188 deletions
diff --git a/Lib/idlelib/config_sec.py b/Lib/idlelib/config_sec.py
deleted file mode 100644
index 7b59124..0000000
--- 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 f57c9a1..6629d70 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 701f4d9..d809d30 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, <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
index a98b484..0000000
--- 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 0000000..e9c4bd4
--- /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 0000000..e3937a1
--- /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 <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)