diff --git a/Lib/idlelib/config_help.py b/Lib/idlelib/config_help.py deleted file mode 100644 index cde8118fe6b..00000000000 --- a/Lib/idlelib/config_help.py +++ /dev/null @@ -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('', 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) diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 6629d70ec6d..388b48f088e 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -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) diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py index 71302d03fa8..f5311e966c4 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." } -GetHelpSourceDialog_spec = { - 'file': 'config_help', - 'kwds': {'title': 'Get helpsource', - '_htest': True}, - 'msg': "Enter menu item name and help file path\n " - " and more than 30 chars are invalid menu item names.\n" - ", 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 . [Cancel] will print None to shell" + } + _io_binding_spec = { 'file': 'iomenu', 'kwds': {}, @@ -241,7 +245,7 @@ Query_spec = { '_htest': True}, 'msg': "Enter with 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]" } diff --git a/Lib/idlelib/idle_test/test_config_help.py b/Lib/idlelib/idle_test/test_config_help.py deleted file mode 100644 index b89b4e3ca13..00000000000 --- a/Lib/idlelib/idle_test/test_config_help.py +++ /dev/null @@ -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) diff --git a/Lib/idlelib/idle_test/test_query.py b/Lib/idlelib/idle_test/test_query.py index 58873c4998c..45c99fac241 100644 --- a/Lib/idlelib/idle_test/test_query.py +++ b/Lib/idlelib/idle_test/test_query.py @@ -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) diff --git a/Lib/idlelib/query.py b/Lib/idlelib/query.py index fd9716f5d4c..d2d1472a0e5 100644 --- a/Lib/idlelib/query.py +++ b/Lib/idlelib/query.py @@ -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 ) moves focus. Move it back. + # [Ok] moves focus. ( 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)