Issue #27380: IDLE: add query.HelpSource class and tests.
Remove modules that are combined in new module.
This commit is contained in:
parent
d6402a40d3
commit
8b22c0aada
|
@ -1,170 +0,0 @@
|
|||
"Dialog to specify or edit the parameters for a user configured help source."
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from tkinter import *
|
||||
import tkinter.messagebox as tkMessageBox
|
||||
import tkinter.filedialog as tkFileDialog
|
||||
|
||||
class GetHelpSourceDialog(Toplevel):
|
||||
def __init__(self, parent, title, menuItem='', filePath='', _htest=False):
|
||||
"""Get menu entry and url/ local file location for Additional Help
|
||||
|
||||
User selects a name for the Help resource and provides a web url
|
||||
or a local file as its source. The user can enter a url or browse
|
||||
for the file.
|
||||
|
||||
_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.result = None
|
||||
self.create_widgets()
|
||||
self.menu.set(menuItem)
|
||||
self.path.set(filePath)
|
||||
self.withdraw() #hide while setting geometry
|
||||
#needs to be done here so that the winfo_reqwidth is valid
|
||||
self.update_idletasks()
|
||||
#centre dialog over parent. below parent if running htest.
|
||||
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)))
|
||||
self.deiconify() #geometry set, unhide
|
||||
self.bind('<Return>', self.ok)
|
||||
self.wait_window()
|
||||
|
||||
def create_widgets(self):
|
||||
self.menu = StringVar(self)
|
||||
self.path = StringVar(self)
|
||||
self.fontSize = StringVar(self)
|
||||
self.frameMain = Frame(self, borderwidth=2, relief=GROOVE)
|
||||
self.frameMain.pack(side=TOP, expand=TRUE, fill=BOTH)
|
||||
labelMenu = Label(self.frameMain, anchor=W, justify=LEFT,
|
||||
text='Menu Item:')
|
||||
self.entryMenu = Entry(self.frameMain, textvariable=self.menu,
|
||||
width=30)
|
||||
self.entryMenu.focus_set()
|
||||
labelPath = Label(self.frameMain, anchor=W, justify=LEFT,
|
||||
text='Help File Path: Enter URL or browse for file')
|
||||
self.entryPath = Entry(self.frameMain, textvariable=self.path,
|
||||
width=40)
|
||||
self.entryMenu.focus_set()
|
||||
labelMenu.pack(anchor=W, padx=5, pady=3)
|
||||
self.entryMenu.pack(anchor=W, padx=5, pady=3)
|
||||
labelPath.pack(anchor=W, padx=5, pady=3)
|
||||
self.entryPath.pack(anchor=W, padx=5, pady=3)
|
||||
browseButton = Button(self.frameMain, text='Browse', width=8,
|
||||
command=self.browse_file)
|
||||
browseButton.pack(pady=3)
|
||||
frameButtons = Frame(self)
|
||||
frameButtons.pack(side=BOTTOM, fill=X)
|
||||
self.buttonOk = Button(frameButtons, text='OK',
|
||||
width=8, default=ACTIVE, command=self.ok)
|
||||
self.buttonOk.grid(row=0, column=0, padx=5,pady=5)
|
||||
self.buttonCancel = Button(frameButtons, text='Cancel',
|
||||
width=8, command=self.cancel)
|
||||
self.buttonCancel.grid(row=0, column=1, padx=5, pady=5)
|
||||
|
||||
def browse_file(self):
|
||||
filetypes = [
|
||||
("HTML Files", "*.htm *.html", "TEXT"),
|
||||
("PDF Files", "*.pdf", "TEXT"),
|
||||
("Windows Help Files", "*.chm"),
|
||||
("Text Files", "*.txt", "TEXT"),
|
||||
("All Files", "*")]
|
||||
path = self.path.get()
|
||||
if path:
|
||||
dir, base = os.path.split(path)
|
||||
else:
|
||||
base = None
|
||||
if sys.platform[:3] == 'win':
|
||||
dir = os.path.join(os.path.dirname(sys.executable), 'Doc')
|
||||
if not os.path.isdir(dir):
|
||||
dir = os.getcwd()
|
||||
else:
|
||||
dir = os.getcwd()
|
||||
opendialog = tkFileDialog.Open(parent=self, filetypes=filetypes)
|
||||
file = opendialog.show(initialdir=dir, initialfile=base)
|
||||
if file:
|
||||
self.path.set(file)
|
||||
|
||||
def menu_ok(self):
|
||||
"Simple validity check for a sensible menu item name"
|
||||
menu_ok = True
|
||||
menu = self.menu.get()
|
||||
menu.strip()
|
||||
if not menu:
|
||||
tkMessageBox.showerror(title='Menu Item Error',
|
||||
message='No menu item specified',
|
||||
parent=self)
|
||||
self.entryMenu.focus_set()
|
||||
menu_ok = False
|
||||
elif len(menu) > 30:
|
||||
tkMessageBox.showerror(title='Menu Item Error',
|
||||
message='Menu item too long:'
|
||||
'\nLimit 30 characters.',
|
||||
parent=self)
|
||||
self.entryMenu.focus_set()
|
||||
menu_ok = False
|
||||
return menu_ok
|
||||
|
||||
def path_ok(self):
|
||||
"Simple validity check for menu file path"
|
||||
path_ok = True
|
||||
path = self.path.get()
|
||||
path.strip()
|
||||
if not path: #no path specified
|
||||
tkMessageBox.showerror(title='File Path Error',
|
||||
message='No help file path specified.',
|
||||
parent=self)
|
||||
self.entryPath.focus_set()
|
||||
path_ok = False
|
||||
elif path.startswith(('www.', 'http')):
|
||||
pass
|
||||
else:
|
||||
if path[:5] == 'file:':
|
||||
path = path[5:]
|
||||
if not os.path.exists(path):
|
||||
tkMessageBox.showerror(title='File Path Error',
|
||||
message='Help file path does not exist.',
|
||||
parent=self)
|
||||
self.entryPath.focus_set()
|
||||
path_ok = False
|
||||
return path_ok
|
||||
|
||||
def ok(self, event=None):
|
||||
if self.menu_ok() and self.path_ok():
|
||||
self.result = (self.menu.get().strip(),
|
||||
self.path.get().strip())
|
||||
if sys.platform == 'darwin':
|
||||
path = self.result[1]
|
||||
if path.startswith(('www', 'file:', 'http:', 'https:')):
|
||||
pass
|
||||
else:
|
||||
# Mac Safari insists on using the URI form for local files
|
||||
self.result = list(self.result)
|
||||
self.result[1] = "file://" + path
|
||||
self.destroy()
|
||||
|
||||
def cancel(self, event=None):
|
||||
self.result = None
|
||||
self.destroy()
|
||||
|
||||
if __name__ == '__main__':
|
||||
import unittest
|
||||
unittest.main('idlelib.idle_test.test_config_help',
|
||||
verbosity=2, exit=False)
|
||||
|
||||
from idlelib.idle_test.htest import run
|
||||
run(GetHelpSourceDialog)
|
|
@ -18,8 +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.query import SectionName
|
||||
from idlelib.config_help import GetHelpSourceDialog
|
||||
from idlelib.query import SectionName, HelpSource
|
||||
from idlelib.tabbedpages import TabbedPageSet
|
||||
from idlelib.textview import view_text
|
||||
from idlelib import macosx
|
||||
|
@ -940,7 +939,8 @@ class ConfigDialog(Toplevel):
|
|||
self.buttonHelpListRemove.config(state=DISABLED)
|
||||
|
||||
def HelpListItemAdd(self):
|
||||
helpSource = GetHelpSourceDialog(self, 'New Help Source').result
|
||||
helpSource = HelpSource(self, 'New Help Source',
|
||||
).result
|
||||
if helpSource:
|
||||
self.userHelpList.append((helpSource[0], helpSource[1]))
|
||||
self.listHelp.insert(END, helpSource[0])
|
||||
|
@ -950,16 +950,17 @@ class ConfigDialog(Toplevel):
|
|||
def HelpListItemEdit(self):
|
||||
itemIndex = self.listHelp.index(ANCHOR)
|
||||
helpSource = self.userHelpList[itemIndex]
|
||||
newHelpSource = GetHelpSourceDialog(
|
||||
self, 'Edit Help Source', menuItem=helpSource[0],
|
||||
filePath=helpSource[1]).result
|
||||
if (not newHelpSource) or (newHelpSource == helpSource):
|
||||
return #no changes
|
||||
self.userHelpList[itemIndex] = newHelpSource
|
||||
self.listHelp.delete(itemIndex)
|
||||
self.listHelp.insert(itemIndex, newHelpSource[0])
|
||||
self.UpdateUserHelpChangedItems()
|
||||
self.SetHelpListButtonStates()
|
||||
newHelpSource = HelpSource(
|
||||
self, 'Edit Help Source',
|
||||
menuitem=helpSource[0],
|
||||
filepath=helpSource[1],
|
||||
).result
|
||||
if newHelpSource and newHelpSource != helpSource:
|
||||
self.userHelpList[itemIndex] = newHelpSource
|
||||
self.listHelp.delete(itemIndex)
|
||||
self.listHelp.insert(itemIndex, newHelpSource[0])
|
||||
self.UpdateUserHelpChangedItems()
|
||||
self.SetHelpListButtonStates()
|
||||
|
||||
def HelpListItemRemove(self):
|
||||
itemIndex = self.listHelp.index(ANCHOR)
|
||||
|
|
|
@ -137,18 +137,6 @@ _editor_window_spec = {
|
|||
"Best to close editor first."
|
||||
}
|
||||
|
||||
GetHelpSourceDialog_spec = {
|
||||
'file': 'config_help',
|
||||
'kwds': {'title': 'Get helpsource',
|
||||
'_htest': True},
|
||||
'msg': "Enter menu item name and help file path\n "
|
||||
"<nothing> and more than 30 chars are invalid menu item names.\n"
|
||||
"<nothing>, file does not exist are invalid path items.\n"
|
||||
"Test for incomplete web address for help file path.\n"
|
||||
"A valid entry will be printed to shell with [0k].\n"
|
||||
"[Cancel] will print None to shell",
|
||||
}
|
||||
|
||||
# Update once issue21519 is resolved.
|
||||
GetKeysDialog_spec = {
|
||||
'file': 'config_key',
|
||||
|
@ -175,6 +163,22 @@ _grep_dialog_spec = {
|
|||
"should open that file \nin a new EditorWindow."
|
||||
}
|
||||
|
||||
HelpSource_spec = {
|
||||
'file': 'query',
|
||||
'kwds': {'title': 'Help name and source',
|
||||
'menuitem': 'test',
|
||||
'filepath': __file__,
|
||||
'used_names': {'abc'},
|
||||
'_htest': True},
|
||||
'msg': "Enter menu item name and help file path\n"
|
||||
"'', > than 30 chars, and 'abc' are invalid menu item names.\n"
|
||||
"'' and file does not exist are invalid path items.\n"
|
||||
"Any url ('www...', 'http...') is accepted.\n"
|
||||
"Test Browse with and without path, as cannot unittest.\n"
|
||||
"A valid entry will be printed to shell with [0k]\n"
|
||||
"or <return>. [Cancel] will print None to shell"
|
||||
}
|
||||
|
||||
_io_binding_spec = {
|
||||
'file': 'iomenu',
|
||||
'kwds': {},
|
||||
|
@ -241,7 +245,7 @@ Query_spec = {
|
|||
'_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]",
|
||||
"Close dialog with valid entry, [Cancel] or [X]"
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
"""Unittests for idlelib.config_help.py"""
|
||||
import unittest
|
||||
from idlelib.idle_test.mock_tk import Var, Mbox, Entry
|
||||
from idlelib import config_help as help_dialog_module
|
||||
|
||||
help_dialog = help_dialog_module.GetHelpSourceDialog
|
||||
|
||||
|
||||
class Dummy_help_dialog:
|
||||
# Mock for testing the following methods of help_dialog
|
||||
menu_ok = help_dialog.menu_ok
|
||||
path_ok = help_dialog.path_ok
|
||||
ok = help_dialog.ok
|
||||
cancel = help_dialog.cancel
|
||||
# Attributes, constant or variable, needed for tests
|
||||
menu = Var()
|
||||
entryMenu = Entry()
|
||||
path = Var()
|
||||
entryPath = Entry()
|
||||
result = None
|
||||
destroyed = False
|
||||
|
||||
def destroy(self):
|
||||
self.destroyed = True
|
||||
|
||||
|
||||
# menu_ok and path_ok call Mbox.showerror if menu and path are not ok.
|
||||
orig_mbox = help_dialog_module.tkMessageBox
|
||||
showerror = Mbox.showerror
|
||||
|
||||
|
||||
class ConfigHelpTest(unittest.TestCase):
|
||||
dialog = Dummy_help_dialog()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
help_dialog_module.tkMessageBox = Mbox
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
help_dialog_module.tkMessageBox = orig_mbox
|
||||
|
||||
def test_blank_menu(self):
|
||||
self.dialog.menu.set('')
|
||||
self.assertFalse(self.dialog.menu_ok())
|
||||
self.assertEqual(showerror.title, 'Menu Item Error')
|
||||
self.assertIn('No', showerror.message)
|
||||
|
||||
def test_long_menu(self):
|
||||
self.dialog.menu.set('hello' * 10)
|
||||
self.assertFalse(self.dialog.menu_ok())
|
||||
self.assertEqual(showerror.title, 'Menu Item Error')
|
||||
self.assertIn('long', showerror.message)
|
||||
|
||||
def test_good_menu(self):
|
||||
self.dialog.menu.set('help')
|
||||
showerror.title = 'No Error' # should not be called
|
||||
self.assertTrue(self.dialog.menu_ok())
|
||||
self.assertEqual(showerror.title, 'No Error')
|
||||
|
||||
def test_blank_path(self):
|
||||
self.dialog.path.set('')
|
||||
self.assertFalse(self.dialog.path_ok())
|
||||
self.assertEqual(showerror.title, 'File Path Error')
|
||||
self.assertIn('No', showerror.message)
|
||||
|
||||
def test_invalid_file_path(self):
|
||||
self.dialog.path.set('foobar' * 100)
|
||||
self.assertFalse(self.dialog.path_ok())
|
||||
self.assertEqual(showerror.title, 'File Path Error')
|
||||
self.assertIn('not exist', showerror.message)
|
||||
|
||||
def test_invalid_url_path(self):
|
||||
self.dialog.path.set('ww.foobar.com')
|
||||
self.assertFalse(self.dialog.path_ok())
|
||||
self.assertEqual(showerror.title, 'File Path Error')
|
||||
self.assertIn('not exist', showerror.message)
|
||||
|
||||
self.dialog.path.set('htt.foobar.com')
|
||||
self.assertFalse(self.dialog.path_ok())
|
||||
self.assertEqual(showerror.title, 'File Path Error')
|
||||
self.assertIn('not exist', showerror.message)
|
||||
|
||||
def test_good_path(self):
|
||||
self.dialog.path.set('https://docs.python.org')
|
||||
showerror.title = 'No Error' # should not be called
|
||||
self.assertTrue(self.dialog.path_ok())
|
||||
self.assertEqual(showerror.title, 'No Error')
|
||||
|
||||
def test_ok(self):
|
||||
self.dialog.destroyed = False
|
||||
self.dialog.menu.set('help')
|
||||
self.dialog.path.set('https://docs.python.org')
|
||||
self.dialog.ok()
|
||||
self.assertEqual(self.dialog.result, ('help',
|
||||
'https://docs.python.org'))
|
||||
self.assertTrue(self.dialog.destroyed)
|
||||
|
||||
def test_cancel(self):
|
||||
self.dialog.destroyed = False
|
||||
self.dialog.cancel()
|
||||
self.assertEqual(self.dialog.result, None)
|
||||
self.assertTrue(self.dialog.destroyed)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2, exit=False)
|
|
@ -1,6 +1,16 @@
|
|||
"""Test idlelib.query.
|
||||
|
||||
Coverage: 100%.
|
||||
Non-gui tests for Query, SectionName, ModuleName, and HelpSource use
|
||||
dummy versions that extract the non-gui methods and add other needed
|
||||
attributes. GUI tests create an instance of each class and simulate
|
||||
entries and button clicks. Subclass tests only target the new code in
|
||||
the subclass definition.
|
||||
|
||||
The appearance of the widgets is checked by the Query and
|
||||
HelpSource htests. These are run by running query.py.
|
||||
|
||||
Coverage: 94% (100% for Query and SectionName).
|
||||
6 of 8 missing are ModuleName exceptions I don't know how to trigger.
|
||||
"""
|
||||
from test.support import requires
|
||||
from tkinter import Tk
|
||||
|
@ -9,21 +19,9 @@ from unittest import mock
|
|||
from idlelib.idle_test.mock_tk import Var, Mbox_func
|
||||
from idlelib import query
|
||||
|
||||
Query = query.Query
|
||||
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
|
||||
# Mock entry.showerror messagebox so don't need click to continue
|
||||
# when entry_ok and path_ok methods call it to display errors.
|
||||
|
||||
# entry_ok calls modal messagebox.showerror if entry is not ok.
|
||||
# Mock showerrer so don't need to click to continue.
|
||||
orig_showerror = query.showerror
|
||||
showerror = Mbox_func() # Instance has __call__ method.
|
||||
|
||||
|
@ -34,7 +32,23 @@ def tearDownModule():
|
|||
query.showerror = orig_showerror
|
||||
|
||||
|
||||
# NON-GUI TESTS
|
||||
|
||||
class QueryTest(unittest.TestCase):
|
||||
"Test Query base class."
|
||||
|
||||
class Dummy_Query:
|
||||
# Test the following Query methods.
|
||||
entry_ok = query.Query.entry_ok
|
||||
ok = query.Query.ok
|
||||
cancel = query.Query.cancel
|
||||
# Add attributes needed for the tests.
|
||||
entry = Var()
|
||||
result = None
|
||||
destroyed = False
|
||||
def destroy(self):
|
||||
self.destroyed = True
|
||||
|
||||
dialog = Dummy_Query()
|
||||
|
||||
def setUp(self):
|
||||
|
@ -42,7 +56,7 @@ class QueryTest(unittest.TestCase):
|
|||
self.dialog.result = None
|
||||
self.dialog.destroyed = False
|
||||
|
||||
def test_blank_entry(self):
|
||||
def test_entry_ok_blank(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.entry.set(' ')
|
||||
|
@ -51,7 +65,7 @@ class QueryTest(unittest.TestCase):
|
|||
Equal(showerror.title, 'Entry Error')
|
||||
self.assertIn('Blank', showerror.message)
|
||||
|
||||
def test_good_entry(self):
|
||||
def test_entry_ok_good(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.entry.set(' good ')
|
||||
|
@ -59,7 +73,17 @@ class QueryTest(unittest.TestCase):
|
|||
Equal((dialog.result, dialog.destroyed), (None, False))
|
||||
Equal(showerror.title, None)
|
||||
|
||||
def test_ok(self):
|
||||
def test_ok_blank(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.entry.set('')
|
||||
dialog.entry.focus_set = mock.Mock()
|
||||
Equal(dialog.ok(), None)
|
||||
self.assertTrue(dialog.entry.focus_set.called)
|
||||
del dialog.entry.focus_set
|
||||
Equal((dialog.result, dialog.destroyed), (None, False))
|
||||
|
||||
def test_ok_good(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.entry.set('good')
|
||||
|
@ -73,12 +97,14 @@ class QueryTest(unittest.TestCase):
|
|||
Equal((dialog.result, dialog.destroyed), (None, True))
|
||||
|
||||
|
||||
class Dummy_SectionName:
|
||||
entry_ok = query.SectionName.entry_ok # Test override.
|
||||
used_names = ['used']
|
||||
entry = Var()
|
||||
|
||||
class SectionNameTest(unittest.TestCase):
|
||||
"Test SectionName subclass of Query."
|
||||
|
||||
class Dummy_SectionName:
|
||||
entry_ok = query.SectionName.entry_ok # Function being tested.
|
||||
used_names = ['used']
|
||||
entry = Var()
|
||||
|
||||
dialog = Dummy_SectionName()
|
||||
|
||||
def setUp(self):
|
||||
|
@ -116,12 +142,14 @@ class SectionNameTest(unittest.TestCase):
|
|||
Equal(showerror.title, None)
|
||||
|
||||
|
||||
class Dummy_ModuleName:
|
||||
entry_ok = query.ModuleName.entry_ok # Test override
|
||||
text0 = ''
|
||||
entry = Var()
|
||||
|
||||
class ModuleNameTest(unittest.TestCase):
|
||||
"Test ModuleName subclass of Query."
|
||||
|
||||
class Dummy_ModuleName:
|
||||
entry_ok = query.ModuleName.entry_ok # Funtion being tested.
|
||||
text0 = ''
|
||||
entry = Var()
|
||||
|
||||
dialog = Dummy_ModuleName()
|
||||
|
||||
def setUp(self):
|
||||
|
@ -159,13 +187,119 @@ class ModuleNameTest(unittest.TestCase):
|
|||
Equal(showerror.title, None)
|
||||
|
||||
|
||||
# 3 HelpSource test classes each test one function.
|
||||
|
||||
orig_platform = query.platform
|
||||
|
||||
class HelpsourceBrowsefileTest(unittest.TestCase):
|
||||
"Test browse_file method of ModuleName subclass of Query."
|
||||
|
||||
class Dummy_HelpSource:
|
||||
browse_file = query.HelpSource.browse_file
|
||||
pathvar = Var()
|
||||
|
||||
dialog = Dummy_HelpSource()
|
||||
|
||||
def test_file_replaces_path(self):
|
||||
# Path is widget entry, file is file dialog return.
|
||||
dialog = self.dialog
|
||||
for path, func, result in (
|
||||
# We need all combination to test all (most) code paths.
|
||||
('', lambda a,b,c:'', ''),
|
||||
('', lambda a,b,c: __file__, __file__),
|
||||
('htest', lambda a,b,c:'', 'htest'),
|
||||
('htest', lambda a,b,c: __file__, __file__)):
|
||||
with self.subTest():
|
||||
dialog.pathvar.set(path)
|
||||
dialog.askfilename = func
|
||||
dialog.browse_file()
|
||||
self.assertEqual(dialog.pathvar.get(), result)
|
||||
|
||||
|
||||
class HelpsourcePathokTest(unittest.TestCase):
|
||||
"Test path_ok method of ModuleName subclass of Query."
|
||||
|
||||
class Dummy_HelpSource:
|
||||
path_ok = query.HelpSource.path_ok
|
||||
path = Var()
|
||||
|
||||
dialog = Dummy_HelpSource()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
query.platform = orig_platform
|
||||
|
||||
def setUp(self):
|
||||
showerror.title = None
|
||||
|
||||
def test_path_ok_blank(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.path.set(' ')
|
||||
Equal(dialog.path_ok(), None)
|
||||
Equal(showerror.title, 'File Path Error')
|
||||
self.assertIn('No help', showerror.message)
|
||||
|
||||
def test_path_ok_bad(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
dialog.path.set(__file__ + 'bad-bad-bad')
|
||||
Equal(dialog.path_ok(), None)
|
||||
Equal(showerror.title, 'File Path Error')
|
||||
self.assertIn('not exist', showerror.message)
|
||||
|
||||
def test_path_ok_web(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
for url in 'www.py.org', 'http://py.org':
|
||||
with self.subTest():
|
||||
dialog.path.set(url)
|
||||
Equal(dialog.path_ok(), url)
|
||||
Equal(showerror.title, None)
|
||||
|
||||
def test_path_ok_file(self):
|
||||
dialog = self.dialog
|
||||
Equal = self.assertEqual
|
||||
for platform, prefix in ('darwin', 'file://'), ('other', ''):
|
||||
with self.subTest():
|
||||
query.platform = platform
|
||||
dialog.path.set(__file__)
|
||||
Equal(dialog.path_ok(), prefix + __file__)
|
||||
Equal(showerror.title, None)
|
||||
|
||||
|
||||
class HelpsourceEntryokTest(unittest.TestCase):
|
||||
"Test entry_ok method of ModuleName subclass of Query."
|
||||
|
||||
class Dummy_HelpSource:
|
||||
entry_ok = query.HelpSource.entry_ok
|
||||
def item_ok(self):
|
||||
return self.name
|
||||
def path_ok(self):
|
||||
return self.path
|
||||
|
||||
dialog = Dummy_HelpSource()
|
||||
|
||||
def test_entry_ok_helpsource(self):
|
||||
dialog = self.dialog
|
||||
for name, path, result in ((None, None, None),
|
||||
(None, 'doc.txt', None),
|
||||
('doc', None, None),
|
||||
('doc', 'doc.txt', ('doc', 'doc.txt'))):
|
||||
with self.subTest():
|
||||
dialog.name, dialog.path = name, path
|
||||
self.assertEqual(self.dialog.entry_ok(), result)
|
||||
|
||||
|
||||
# GUI TESTS
|
||||
|
||||
class QueryGuiTest(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
requires('gui')
|
||||
cls.root = root = Tk()
|
||||
cls.dialog = Query(root, 'TEST', 'test', _utest=True)
|
||||
cls.dialog = query.Query(root, 'TEST', 'test', _utest=True)
|
||||
cls.dialog.destroy = mock.Mock()
|
||||
|
||||
@classmethod
|
||||
|
@ -238,5 +372,25 @@ class ModulenameGuiTest(unittest.TestCase):
|
|||
del root
|
||||
|
||||
|
||||
class HelpsourceGuiTest(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
requires('gui')
|
||||
|
||||
def test_click_help_source(self):
|
||||
root = Tk()
|
||||
dialog = query.HelpSource(root, 'T', menuitem='__test__',
|
||||
filepath=__file__, _utest=True)
|
||||
Equal = self.assertEqual
|
||||
Equal(dialog.entry.get(), '__test__')
|
||||
Equal(dialog.path.get(), __file__)
|
||||
dialog.button_ok.invoke()
|
||||
Equal(dialog.result, ('__test__', __file__))
|
||||
del dialog
|
||||
root.destroy()
|
||||
del root
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2, exit=False)
|
||||
|
|
|
@ -13,10 +13,16 @@ 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.
|
||||
# generic and specific parts. 3.6 only, July 2016.
|
||||
# ModuleName.entry_ok came from editor.EditorWindow.load_module.
|
||||
# HelpSource was extracted from configHelpSourceEdit.py (temporarily
|
||||
# config_help.py), with darwin code moved from ok to path_ok.
|
||||
|
||||
import importlib
|
||||
import os
|
||||
from sys import executable, platform # Platform is set for one test.
|
||||
from tkinter import Toplevel, StringVar
|
||||
from tkinter import filedialog
|
||||
from tkinter.messagebox import showerror
|
||||
from tkinter.ttk import Frame, Button, Entry, Label
|
||||
|
||||
|
@ -25,8 +31,8 @@ class Query(Toplevel):
|
|||
|
||||
For this base class, accept any non-blank string.
|
||||
"""
|
||||
def __init__(self, parent, title, message, text0='',
|
||||
*, _htest=False, _utest=False):
|
||||
def __init__(self, parent, title, message, *, text0='', used_names={},
|
||||
_htest=False, _utest=False):
|
||||
"""Create popup, do not return until tk widget destroyed.
|
||||
|
||||
Additional subclass init must be done before calling this
|
||||
|
@ -35,10 +41,12 @@ class Query(Toplevel):
|
|||
title - string, title of popup dialog
|
||||
message - string, informational message to display
|
||||
text0 - initial value for entry
|
||||
used_names - names already in use
|
||||
_htest - bool, change box location when running htest
|
||||
_utest - bool, leave window hidden and not modal
|
||||
"""
|
||||
Toplevel.__init__(self, parent)
|
||||
self.withdraw() # Hide while configuring, especially geometry.
|
||||
self.configure(borderwidth=5)
|
||||
self.resizable(height=False, width=False)
|
||||
self.title(title)
|
||||
|
@ -49,27 +57,26 @@ class Query(Toplevel):
|
|||
self.parent = parent
|
||||
self.message = message
|
||||
self.text0 = text0
|
||||
self.used_names = used_names
|
||||
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(
|
||||
self.update_idletasks() # Needed here for winfo_reqwidth below.
|
||||
self.geometry( # Center dialog over parent (or below htest box).
|
||||
"+%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.deiconify() # Unhide now that geometry set.
|
||||
self.wait_window()
|
||||
|
||||
def create_widgets(self): # Call from override, if any.
|
||||
# Bind widgets needed for entry_ok or unittest to self.
|
||||
frame = Frame(self, borderwidth=2, relief='sunken', )
|
||||
label = Label(frame, anchor='w', justify='left',
|
||||
text=self.message)
|
||||
# Bind to self widgets needed for entry_ok or unittest.
|
||||
self.frame = frame = Frame(self, borderwidth=2, relief='sunken', )
|
||||
entrylabel = Label(frame, anchor='w', justify='left',
|
||||
text=self.message)
|
||||
self.entryvar = StringVar(self, self.text0)
|
||||
self.entry = Entry(frame, width=30, textvariable=self.entryvar)
|
||||
self.entry.focus_set()
|
||||
|
@ -81,7 +88,7 @@ class Query(Toplevel):
|
|||
width=8, command=self.cancel)
|
||||
|
||||
frame.pack(side='top', expand=True, fill='both')
|
||||
label.pack(padx=5, pady=5)
|
||||
entrylabel.pack(padx=5, pady=5)
|
||||
self.entry.pack(padx=5, pady=5)
|
||||
buttons.pack(side='bottom')
|
||||
self.button_ok.pack(side='left', padx=5)
|
||||
|
@ -93,7 +100,7 @@ class Query(Toplevel):
|
|||
if not entry:
|
||||
showerror(title='Entry Error',
|
||||
message='Blank line.', parent=self)
|
||||
return
|
||||
return None
|
||||
return entry
|
||||
|
||||
def ok(self, event=None): # Do not replace.
|
||||
|
@ -106,7 +113,7 @@ class Query(Toplevel):
|
|||
self.result = entry
|
||||
self.destroy()
|
||||
else:
|
||||
# [Ok] (but not <Return>) moves focus. Move it back.
|
||||
# [Ok] moves focus. (<Return> does not.) Move it back.
|
||||
self.entry.focus_set()
|
||||
|
||||
def cancel(self, event=None): # Do not replace.
|
||||
|
@ -117,13 +124,12 @@ class Query(Toplevel):
|
|||
|
||||
class SectionName(Query):
|
||||
"Get a name for a config file section name."
|
||||
# Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
|
||||
|
||||
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)
|
||||
super().__init__(parent, title, message, used_names=used_names,
|
||||
_htest=_htest, _utest=_utest)
|
||||
|
||||
def entry_ok(self):
|
||||
"Return sensible ConfigParser section name or None."
|
||||
|
@ -131,16 +137,16 @@ class SectionName(Query):
|
|||
if not name:
|
||||
showerror(title='Name Error',
|
||||
message='No name specified.', parent=self)
|
||||
return
|
||||
return None
|
||||
elif len(name)>30:
|
||||
showerror(title='Name Error',
|
||||
message='Name too long. It should be no more than '+
|
||||
'30 characters.', parent=self)
|
||||
return
|
||||
return None
|
||||
elif name in self.used_names:
|
||||
showerror(title='Name Error',
|
||||
message='This name is already in use.', parent=self)
|
||||
return
|
||||
return None
|
||||
return name
|
||||
|
||||
|
||||
|
@ -148,48 +154,133 @@ class ModuleName(Query):
|
|||
"Get a module name for Open Module menu entry."
|
||||
# Used in open_module (editor.EditorWindow until move to iobinding).
|
||||
|
||||
def __init__(self, parent, title, message, text0='',
|
||||
def __init__(self, parent, title, message, text0,
|
||||
*, _htest=False, _utest=False):
|
||||
"""text0 - name selected in text before Open Module invoked"
|
||||
"""
|
||||
Query.__init__(self, parent, title, message, text0=text0,
|
||||
_htest=_htest, _utest=_utest)
|
||||
super().__init__(parent, title, message, text0=text0,
|
||||
_htest=_htest, _utest=_utest)
|
||||
|
||||
def entry_ok(self):
|
||||
"Return entered module name as file path or None."
|
||||
# Moved here from Editor_Window.load_module 2016 July.
|
||||
name = self.entry.get().strip()
|
||||
if not name:
|
||||
showerror(title='Name Error',
|
||||
message='No name specified.', parent=self)
|
||||
return
|
||||
# XXX Ought to insert current file's directory in front of path
|
||||
return None
|
||||
# XXX Ought to insert current file's directory in front of path.
|
||||
try:
|
||||
spec = importlib.util.find_spec(name)
|
||||
except (ValueError, ImportError) as msg:
|
||||
showerror("Import Error", str(msg), parent=self)
|
||||
return
|
||||
return None
|
||||
if spec is None:
|
||||
showerror("Import Error", "module not found",
|
||||
parent=self)
|
||||
return
|
||||
return None
|
||||
if not isinstance(spec.loader, importlib.abc.SourceLoader):
|
||||
showerror("Import Error", "not a source-based module",
|
||||
parent=self)
|
||||
return
|
||||
return None
|
||||
try:
|
||||
file_path = spec.loader.get_filename(name)
|
||||
except AttributeError:
|
||||
showerror("Import Error",
|
||||
"loader does not support get_filename",
|
||||
parent=self)
|
||||
return
|
||||
return None
|
||||
return file_path
|
||||
|
||||
|
||||
class HelpSource(Query):
|
||||
"Get menu name and help source for Help menu."
|
||||
# Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
|
||||
|
||||
def __init__(self, parent, title, *, menuitem='', filepath='',
|
||||
used_names={}, _htest=False, _utest=False):
|
||||
"""Get menu entry and url/local file for Additional Help.
|
||||
|
||||
User enters a name for the Help resource and a web url or file
|
||||
name. The user can browse for the file.
|
||||
"""
|
||||
self.filepath = filepath
|
||||
message = 'Name for item on Help menu:'
|
||||
super().__init__(parent, title, message, text0=menuitem,
|
||||
used_names=used_names, _htest=_htest, _utest=_utest)
|
||||
|
||||
def create_widgets(self):
|
||||
super().create_widgets()
|
||||
frame = self.frame
|
||||
pathlabel = Label(frame, anchor='w', justify='left',
|
||||
text='Help File Path: Enter URL or browse for file')
|
||||
self.pathvar = StringVar(self, self.filepath)
|
||||
self.path = Entry(frame, textvariable=self.pathvar, width=40)
|
||||
browse = Button(frame, text='Browse', width=8,
|
||||
command=self.browse_file)
|
||||
|
||||
pathlabel.pack(anchor='w', padx=5, pady=3)
|
||||
self.path.pack(anchor='w', padx=5, pady=3)
|
||||
browse.pack(pady=3)
|
||||
|
||||
def askfilename(self, filetypes, initdir, initfile): # htest #
|
||||
# Extracted from browse_file so can mock for unittests.
|
||||
# Cannot unittest as cannot simulate button clicks.
|
||||
# Test by running htest, such as by running this file.
|
||||
return filedialog.Open(parent=self, filetypes=filetypes)\
|
||||
.show(initialdir=initdir, initialfile=initfile)
|
||||
|
||||
def browse_file(self):
|
||||
filetypes = [
|
||||
("HTML Files", "*.htm *.html", "TEXT"),
|
||||
("PDF Files", "*.pdf", "TEXT"),
|
||||
("Windows Help Files", "*.chm"),
|
||||
("Text Files", "*.txt", "TEXT"),
|
||||
("All Files", "*")]
|
||||
path = self.pathvar.get()
|
||||
if path:
|
||||
dir, base = os.path.split(path)
|
||||
else:
|
||||
base = None
|
||||
if platform[:3] == 'win':
|
||||
dir = os.path.join(os.path.dirname(executable), 'Doc')
|
||||
if not os.path.isdir(dir):
|
||||
dir = os.getcwd()
|
||||
else:
|
||||
dir = os.getcwd()
|
||||
file = self.askfilename(filetypes, dir, base)
|
||||
if file:
|
||||
self.pathvar.set(file)
|
||||
|
||||
item_ok = SectionName.entry_ok # localize for test override
|
||||
|
||||
def path_ok(self):
|
||||
"Simple validity check for menu file path"
|
||||
path = self.path.get().strip()
|
||||
if not path: #no path specified
|
||||
showerror(title='File Path Error',
|
||||
message='No help file path specified.',
|
||||
parent=self)
|
||||
return None
|
||||
elif not path.startswith(('www.', 'http')):
|
||||
if path[:5] == 'file:':
|
||||
path = path[5:]
|
||||
if not os.path.exists(path):
|
||||
showerror(title='File Path Error',
|
||||
message='Help file path does not exist.',
|
||||
parent=self)
|
||||
return None
|
||||
if platform == 'darwin': # for Mac Safari
|
||||
path = "file://" + path
|
||||
return path
|
||||
|
||||
def entry_ok(self):
|
||||
"Return apparently valid (name, path) or None"
|
||||
name = self.item_ok()
|
||||
path = self.path_ok()
|
||||
return None if name is None or path is None else (name, path)
|
||||
|
||||
|
||||
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)
|
||||
run(Query, HelpSource)
|
||||
|
|
Loading…
Reference in New Issue