]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
Issue #27380: IDLE: add query.HelpSource class and tests.
authorTerry Jan Reedy <tjreedy@udel.edu>
Fri, 8 Jul 2016 04:22:50 +0000 (00:22 -0400)
committerTerry Jan Reedy <tjreedy@udel.edu>
Fri, 8 Jul 2016 04:22:50 +0000 (00:22 -0400)
Remove modules that are combined in new module.

Lib/idlelib/config_help.py [deleted file]
Lib/idlelib/configdialog.py
Lib/idlelib/idle_test/htest.py
Lib/idlelib/idle_test/test_config_help.py [deleted file]
Lib/idlelib/idle_test/test_query.py
Lib/idlelib/query.py

diff --git a/Lib/idlelib/config_help.py b/Lib/idlelib/config_help.py
deleted file mode 100644 (file)
index cde8118..0000000
+++ /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('<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)
index 6629d70ec6d68468e11c679431b33fea0673d1e3..388b48f088e95500530c38a6b5b51905e9f68177 100644 (file)
@@ -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)
index 71302d03fa8152a1827196b04916613bf6ee4a1a..f5311e966c445dfe381c2e63f86e249b56cfcb3c 100644 (file)
@@ -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]"
     }
 
 
diff --git a/Lib/idlelib/idle_test/test_config_help.py b/Lib/idlelib/idle_test/test_config_help.py
deleted file mode 100644 (file)
index b89b4e3..0000000
+++ /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)
index 58873c4998ccfa03cb55d3adac2f2c0319106066..45c99fac241a5362f078b173f1fafd8409357b75 100644 (file)
@@ -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
-
-# entry_ok calls modal messagebox.showerror if entry is not ok.
-# Mock showerrer so don't need to click to continue.
+# Mock entry.showerror messagebox so don't need click to continue
+# when entry_ok and path_ok methods call it to display errors.
+
 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)
index fd9716f5d4c0dd9fc900b43737c0b806b4bb7b5f..d2d1472a0e529a6f2f48f2dd5fca85641a9174a4 100644 (file)
@@ -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)