From c21d67984c02795e885704e8ca2cfb19b26f63c5 Mon Sep 17 00:00:00 2001
From: Cheryl Sabella <cheryl.sabella@gmail.com>
Date: Sun, 17 Sep 2017 19:26:16 -0400
Subject: [PATCH 1/9] IDLE: Add docstrings and tests for editor.py

---
 Lib/idlelib/editor.py                | 134 ++++++++-
 Lib/idlelib/idle_test/test_editor.py | 422 ++++++++++++++++++++++++++-
 2 files changed, 539 insertions(+), 17 deletions(-)

diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py
index 855d375055653a..d9824e4200afc5 100644
--- a/Lib/idlelib/editor.py
+++ b/Lib/idlelib/editor.py
@@ -414,6 +414,21 @@ def set_line_and_column(self, event=None):
 
 
     def createmenubar(self):
+        """Populate the menu bar widget for the editor window.
+
+        Each option on the menubar is itself a cascade-type Menu widget
+        with the menubar as the parent.  The names, labels, and menu
+        shortcuts for the menubar items are stored in menu_specs.  Each
+        submenu is subsequently populated in fill_menus(), except for
+        'Recent Files' which is added to the File menu here.
+
+        Instance variables:
+        menubar: Menu widget containing first level menu items.
+        menudict: Dictionary of {menuname: Menu instance} items.  The keys
+            represent the valid menu items for this window and may be a
+            subset of all the menudefs available.
+        recent_files_menu: Menu widget contained within the 'file' menudict.
+        """
         mbar = self.menubar
         self.menudict = menudict = {}
         for name, label in self.menu_specs:
@@ -760,7 +775,13 @@ def ResetFont(self):
         self.text['font'] = idleConf.GetFont(self.root, 'main','EditorWindow')
 
     def RemoveKeybindings(self):
-        "Remove the keybindings before they are changed."
+        """Remove the virtual, configurable keybindings.
+
+        This should be called before the keybindings are applied
+        in ApplyKeyBindings() otherwise the old bindings will still exist.
+        Note: this does not remove the Tk/Tcl keybindings attached to
+        Text widgets by default.
+        """
         # Called from configdialog.py
         self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
         for event, keylist in keydefs.items():
@@ -772,7 +793,12 @@ def RemoveKeybindings(self):
                     self.text.event_delete(event, *keylist)
 
     def ApplyKeybindings(self):
-        "Update the keybindings after they are changed"
+        """Apply the virtual, configurable keybindings.
+
+        The binding events are attached to self.text.  Also, the
+        menu accelerator keys are updated to match the current
+        configuration.
+        """
         # Called from configdialog.py
         self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
         self.apply_bindings()
@@ -780,7 +806,8 @@ def ApplyKeybindings(self):
             xkeydefs = idleConf.GetExtensionBindings(extensionName)
             if xkeydefs:
                 self.apply_bindings(xkeydefs)
-        #update menu accelerators
+        # Update menu accelerators.
+        # XXX - split into its own function and call it from here?
         menuEventDict = {}
         for menu in self.mainmenu.menudefs:
             menuEventDict[menu[0]] = {}
@@ -815,24 +842,35 @@ def set_notabs_indentwidth(self):
                                                   type='int')
 
     def reset_help_menu_entries(self):
-        "Update the additional help entries on the Help menu"
+        """Update the additional help entries on the Help menu.
+
+        First the existing additional help entries are removed from
+        the help menu, then the new help entries are added from idleConf.
+        """
         help_list = idleConf.GetAllExtraHelpSourcesList()
         helpmenu = self.menudict['help']
-        # first delete the extra help entries, if any
+        # First delete the extra help entries, if any.
         helpmenu_length = helpmenu.index(END)
         if helpmenu_length > self.base_helpmenu_length:
             helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
-        # then rebuild them
+        # Then rebuild them.
         if help_list:
             helpmenu.add_separator()
             for entry in help_list:
                 cmd = self.__extra_help_callback(entry[1])
                 helpmenu.add_command(label=entry[0], command=cmd)
-        # and update the menu dictionary
+        # And update the menu dictionary.
         self.menudict['help'] = helpmenu
 
     def __extra_help_callback(self, helpfile):
-        "Create a callback with the helpfile value frozen at definition time"
+        """Create a callback with the helpfile value frozen at definition time.
+
+        Args:
+            helpfile: Filename or website to open.
+
+        Returns:
+            Function to open the helpfile.
+        """
         def display_extra_help(helpfile=helpfile):
             if not helpfile.startswith(('www', 'http')):
                 helpfile = os.path.normpath(helpfile)
@@ -840,7 +878,7 @@ def display_extra_help(helpfile=helpfile):
                 try:
                     os.startfile(helpfile)
                 except OSError as why:
-                    tkMessageBox.showerror(title='Document Start Failure',
+                    self.showerror(title='Document Start Failure',
                         message=str(why), parent=self.text)
             else:
                 webbrowser.open(helpfile)
@@ -999,6 +1037,8 @@ def _close(self):
         if self.color:
             self.color.close(False)
             self.color = None
+        # Allow code context to close its text.after calls.
+        self.text.unbind('<<toggle-code-context>>')
         self.text = None
         self.tkinter_vars = None
         self.per.close()
@@ -1062,6 +1102,11 @@ def load_extension(self, name):
                     self.text.bind(vevent, getattr(ins, methodname))
 
     def apply_bindings(self, keydefs=None):
+        """Add the event bindings in keydefs to self.text.
+
+        Args:
+            keydefs: Virtual events and keybinding definitions.
+        """
         if keydefs is None:
             keydefs = self.mainmenu.default_keydefs
         text = self.text
@@ -1071,9 +1116,28 @@ def apply_bindings(self, keydefs=None):
                 text.event_add(event, *keylist)
 
     def fill_menus(self, menudefs=None, keydefs=None):
-        """Add appropriate entries to the menus and submenus
-
-        Menus that are absent or None in self.menudict are ignored.
+        """Add appropriate entries to the menus and submenus.
+
+        The default menudefs and keydefs are loaded from idlelib.mainmenu.
+        Menus that are absent or None in self.menudict are ignored.  The
+        default menu type created for submenus from menudefs is `command`.
+        A submenu item of None results in a `separator` menu type.
+        A submenu name beginning with ! represents a `checkbutton` type.
+
+        The menus are stored in self.menudict.
+
+        Args:
+            menudefs: Menu and submenu names, underlines (shortcuts),
+                and events which is a list of tuples of the form:
+                [(menu1, [(submenu1a, '<<virtual event>>'),
+                          (submenu1b, '<<virtual event>>'), ...]),
+                 (menu2, [(submenu2a, '<<virtual event>>'),
+                          (submenu2b, '<<virtual event>>'), ...]),
+                ]
+            keydefs: Virtual events and keybinding definitions.  Used for
+                the 'accelerator' text on the menu.  Stored as a
+                dictionary of
+                {'<<virtual event>>': ['<binding1>', '<binding2>'],}
         """
         if menudefs is None:
             menudefs = self.mainmenu.menudefs
@@ -1123,6 +1187,17 @@ def setvar(self, name, value, vartype=None):
             raise NameError(name)
 
     def get_var_obj(self, name, vartype=None):
+        """Return a tkinter variable instance for the event.
+
+        Cache vars in self.tkinter_vars as {name: Var instance}.
+
+        Args:
+            name: Event name.
+            vartype: Tkinter Var type.
+
+        Returns:
+            Tkinter Var instance.
+        """
         var = self.tkinter_vars.get(name)
         if not var and vartype:
             # create a Tkinter variable object with self.text as master:
@@ -1630,8 +1705,16 @@ def run(self):
 ### end autoindent code ###
 
 def prepstr(s):
-    # Helper to extract the underscore from a string, e.g.
-    # prepstr("Co_py") returns (2, "Copy").
+    """Extract the underscore from a string.
+
+    For example, prepstr("Co_py") returns (2, "Copy").
+
+    Args:
+        s: String with underscore.
+
+    Returns:
+        Tuple of (position of underscore, string without underscore).
+    """
     i = s.find('_')
     if i >= 0:
         s = s[:i] + s[i+1:]
@@ -1645,6 +1728,18 @@ def prepstr(s):
 }
 
 def get_accelerator(keydefs, eventname):
+    """Return a formatted string for the keybinding of an event.
+
+    Convert the first keybinding for a given event to a form that
+    can be displayed as an accelerator on the menu.
+
+    Args:
+        keydefs: Dictionary of valid events to keybindings.
+        eventname: Event to retrieve keybinding for.
+
+    Returns:
+        Formatted string of the keybinding.
+    """
     keylist = keydefs.get(eventname)
     # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5
     # if not keylist:
@@ -1654,14 +1749,23 @@ def get_accelerator(keydefs, eventname):
                             "<<change-indentwidth>>"}):
         return ""
     s = keylist[0]
+    # Convert strings of the form -singlelowercase to -singleuppercase.
     s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
+    # Convert certain keynames to their symbol.
     s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
+    # Remove Key- from string.
     s = re.sub("Key-", "", s)
-    s = re.sub("Cancel","Ctrl-Break",s)   # dscherer@cmu.edu
+    # Convert Cancel to Ctrl-Break.
+    s = re.sub("Cancel", "Ctrl-Break", s)   # dscherer@cmu.edu
+    # Convert Control to Ctrl-.
     s = re.sub("Control-", "Ctrl-", s)
+    # Change - to +.
     s = re.sub("-", "+", s)
+    # Change >< to space.
     s = re.sub("><", " ", s)
+    # Remove <.
     s = re.sub("<", "", s)
+    # Remove >.
     s = re.sub(">", "", s)
     return s
 
diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py
index 64a2a88b7e3765..5c2e3cf4f4f423 100644
--- a/Lib/idlelib/idle_test/test_editor.py
+++ b/Lib/idlelib/idle_test/test_editor.py
@@ -1,14 +1,432 @@
+""" Test idlelib.editor.
+"""
+
 import unittest
-from idlelib.editor import EditorWindow
+import tkinter as tk
+import sys
+from functools import partial
+from idlelib import editor
+from idlelib.multicall import MultiCallCreator
+from test.support import requires
+from unittest import mock
+
+root = None
+editwin = None
+
+
+def setUpModule():
+    global root, editwin
+    requires('gui')
+    root = tk.Tk()
+    root.withdraw()
+    editwin = editor.EditorWindow(root=root)
+
+
+def tearDownModule():
+    global root, editwin
+    editwin.close()
+    del editwin
+    root.update_idletasks()
+    root.destroy()
+    del root
+
 
 class Editor_func_test(unittest.TestCase):
     def test_filename_to_unicode(self):
-        func = EditorWindow._filename_to_unicode
+        func = editor.EditorWindow._filename_to_unicode
         class dummy(): filesystemencoding = 'utf-8'
         pairs = (('abc', 'abc'), ('a\U00011111c', 'a\ufffdc'),
                  (b'abc', 'abc'), (b'a\xf0\x91\x84\x91c', 'a\ufffdc'))
         for inp, out in pairs:
             self.assertEqual(func(dummy, inp), out)
 
+
+class ModuleHelpersTest(unittest.TestCase):
+    """Test functions defined at the module level."""
+
+    def test_prepstr(self):
+        ps = editor.prepstr
+        eq = self.assertEqual
+        eq(ps('_spam'), (0, 'spam'))
+        eq(ps('spam'), (-1, 'spam'))
+        eq(ps('spam_'), (4, 'spam'))
+
+    @mock.patch.object(editor.macosx, 'isCocoaTk')
+    def test_get_accelerator(self, mock_cocoa):
+        ga = editor.get_accelerator
+        eq = self.assertEqual
+        keydefs = {'<<zoom-height>>': ['<Alt-Key-9>'],
+                   '<<open-module>>': ['<Control-Shift-O>'],
+                   '<<cancel>>': ['<Cancel>'],
+                   '<<indent>>': ['<Alt-bracketleft>'],
+                   '<<copy>>': ['<Control-j>', '<Control-c>']}
+
+        mock_cocoa.return_value = False
+        eq(ga(keydefs, '<<paste>>'), '')  # Not in keydefs.
+        eq(ga(keydefs, '<<copy>>'), 'Ctrl+J')  # Control to Ctrl and first only.
+        eq(ga(keydefs, '<<zoom-height>>'), 'Alt+9')  # Remove Key-.
+        eq(ga(keydefs, '<<indent>>'), 'Alt+[')  # bracketleft to [.
+        eq(ga(keydefs, '<<cancel>>'), 'Ctrl+Break')  # Cancel to Ctrl-Break.
+        eq(ga(keydefs, '<<open-module>>'), 'Ctrl+Shift+O')  # Shift doesn't change.
+
+        # Cocoa test.
+        mock_cocoa.return_value = True
+        eq(ga(keydefs, '<<open-module>>'), '')  # Cocoa skips open-module shortcut.
+
+
+class MenubarTest(unittest.TestCase):
+    """Test functions involved with creating the menubar."""
+
+    @classmethod
+    def setUpClass(cls):
+        # Test the functions called during the __init__ for
+        # EditorWindow that create the menubar and submenus.
+        # The class is mocked in order to prevent the functions
+        # from being called automatically.
+        w = cls.mock_editwin = mock.Mock(editor.EditorWindow)
+        w.menubar = tk.Menu(root, tearoff=False)
+        w.text = tk.Text(root)
+        w.tkinter_vars = {}
+
+    @classmethod
+    def tearDownClass(cls):
+        w = cls.mock_editwin
+        w.text.destroy()
+        w.menubar.destroy()
+        del w.menubar, w.text, w
+
+    @mock.patch.object(editor.macosx, 'isCarbonTk')
+    def test_createmenubar(self, mock_mac):
+        eq = self.assertEqual
+        ed = editor.EditorWindow
+        w = self.mock_editwin
+        # Call real function instead of mock.
+        cmb = partial(editor.EditorWindow.createmenubar, w)
+
+        # Load real editor menus.
+        w.menu_specs = ed.menu_specs
+
+        mock_mac.return_value = False
+        cmb()
+        eq(list(w.menudict.keys()),
+           [name[0] for name in w.menu_specs])
+        for index in range(w.menubar.index('end') + 1):
+            eq(w.menubar.type(index), tk.CASCADE)
+            eq(w.menubar.entrycget(index, 'label'),
+               editor.prepstr(w.menu_specs[index][1])[1])
+        # Recent Files added here and not fill_menus.
+        eq(w.menudict['file'].entrycget(3, 'label'), 'Recent Files')
+        # No items added to helpmenu, so the length has no value.
+        eq(w.base_helpmenu_length, None)
+        w.fill_menus.assert_called_with()
+        w.reset_help_menu_entries.assert_called_with()
+
+        # Carbon includes an application menu.
+        mock_mac.return_value = True
+        cmb()
+        eq(list(w.menudict.keys()),
+           [name[0] for name in w.menu_specs] + ['application'])
+
+    def test_fill_menus(self):
+        eq = self.assertEqual
+        ed = editor.EditorWindow
+        w = self.mock_editwin
+        # Call real functions instead of mock.
+        fm = partial(editor.EditorWindow.fill_menus, w)
+        w.get_var_obj = ed.get_var_obj.__get__(w)
+
+        # Initialize top level menubar.
+        w.menudict = {}
+        edit = w.menudict['edit'] = tk.Menu(w.menubar, name='edit', tearoff=False)
+        win = w.menudict['windows'] = tk.Menu(w.menubar, name='windows', tearoff=False)
+        form = w.menudict['format'] = tk.Menu(w.menubar, name='format', tearoff=False)
+
+        # Submenus.
+        menudefs = [('edit', [('_New', '<<open-new>>'),
+                              None,
+                              ('!Deb_ug', '<<debug>>')]),
+                    ('shell', [('_View', '<<view-restart>>'), ]),
+                    ('windows', [('Zoom Height', '<<zoom-height>>')]), ]
+        keydefs = {'<<zoom-height>>': ['<Alt-Key-9>']}
+
+        fm(menudefs, keydefs)
+        eq(edit.type(0), tk.COMMAND)
+        eq(edit.entrycget(0, 'label'), 'New')
+        eq(edit.entrycget(0, 'underline'), 0)
+        self.assertIsNotNone(edit.entrycget(0, 'command'))
+        with self.assertRaises(tk.TclError):
+            self.assertIsNone(edit.entrycget(0, 'var'))
+
+        eq(edit.type(1), tk.SEPARATOR)
+        with self.assertRaises(tk.TclError):
+            self.assertIsNone(edit.entrycget(1, 'label'))
+
+        eq(edit.type(2), tk.CHECKBUTTON)
+        eq(edit.entrycget(2, 'label'), 'Debug')  # Strip !.
+        eq(edit.entrycget(2, 'underline'), 3)  # Check that underline ignores !.
+        self.assertIsNotNone(edit.entrycget(2, 'var'))
+        self.assertIn('<<debug>>', w.tkinter_vars)
+
+        eq(win.entrycget(0, 'underline'), -1)
+        eq(win.entrycget(0, 'accelerator'), 'Alt+9')
+
+        self.assertNotIn('shell', w.menudict)
+
+        # Test defaults.
+        w.mainmenu.menudefs = ed.mainmenu.menudefs
+        w.mainmenu.default_keydefs = ed.mainmenu.default_keydefs
+        fm()
+        eq(form.index('end'), 9)  # Default Format menu has 10 items.
+        self.assertNotIn('run', w.menudict)
+
+    @mock.patch.object(editor.idleConf, 'GetAllExtraHelpSourcesList')
+    def test_reset_help_menu_entries(self, mock_extrahelp):
+        w = self.mock_editwin
+        mock_extrahelp.return_value = [('Python', 'https://python.org', '1')]
+        mock_callback = w._EditorWindow__extra_help_callback
+
+        # Create help menu.
+        help = w.menudict['help'] = tk.Menu(w.menubar, name='help', tearoff=False)
+        cmd = mock_callback.return_value = lambda e: 'break'
+        help.add_command(label='help1', command=cmd)
+        w.base_helpmenu_length = help.index('end')
+
+        # Add extra menu items that will be removed.
+        help.add_command(label='extra1', command=cmd)
+        help.add_command(label='extra2', command=cmd)
+        help.add_command(label='extra3', command=cmd)
+        help.add_command(label='extra4', command=cmd)
+
+        # Assert that there are extra help items.
+        self.assertTrue(help.index('end') - w.base_helpmenu_length >= 4)
+        self.assertNotEqual(help.index('end'), w.base_helpmenu_length)
+        editor.EditorWindow.reset_help_menu_entries(w)
+        # Count is 2 because of separator.
+        self.assertEqual(help.index('end') - w.base_helpmenu_length, 2)
+        mock_callback.assert_called_with('https://python.org')
+
+    def test_get_var_obj(self):
+        w = self.mock_editwin
+        gvo = partial(editor.EditorWindow.get_var_obj, w)
+        w.tkinter_vars = {}
+
+        # No vartype.
+        self.assertIsNone(gvo('<<spam>>'))
+        self.assertNotIn('<<spam>>', w.tkinter_vars)
+
+        # Create BooleanVar.
+        self.assertIsInstance(gvo('<<toggle-debugger>>', tk.BooleanVar),
+                              tk.BooleanVar)
+        self.assertIn('<<toggle-debugger>>', w.tkinter_vars)
+
+        # No vartype - check cache.
+        self.assertIsInstance(gvo('<<toggle-debugger>>'), tk.BooleanVar)
+
+    @mock.patch.object(editor.webbrowser, 'open')
+    @unittest.skipIf(sys.platform.startswith('win'), 'this is test for nix system')
+    def test__extra_help_callback_not_windows(self, mock_openfile):
+        w = self.mock_editwin
+        ehc = partial(w._EditorWindow__extra_help_callback, w)
+
+        ehc('http://python.org')
+        mock_openfile.called_with('http://python.org')
+        ehc('www.python.org')
+        mock_openfile.called_with('www.python.org')
+        ehc('/foo/bar/baz/')
+        mock_openfile.called_with('/foo/bar/baz')
+
+    @mock.patch.object(editor.os, 'startfile')
+    @unittest.skipIf(not sys.platform.startswith('win'), 'this is test for windows system')
+    def test__extra_help_callback_windows(self, mock_openfile):
+        # os.startfile doesn't exist on other platforms.
+        w = self.mock_editwin
+        ehc = partial(w._EditorWindow__extra_help_callback, w)
+
+        ehc('http://python.org')
+        mock_openfile.called_with('http://python.org')
+        ehc('www.python.org')
+        mock_openfile.called_with('www.python.org')
+        # Filename that opens successfully.
+        mock_openfile.return_value = True
+        ehc('/foo/bar/baz/')
+        mock_openfile.called_with('\\foo\\bar\\baz')
+        # Filename that doesn't open.
+        mock_openfile.return_value = False
+        with self.assertRaises(OSError):
+            ehc('/foo/bar/baz/')
+        self.assertEqual(w.showerror.title, 'Document Start Failure')
+
+
+class BindingsTest(unittest.TestCase):
+
+    def test_apply_bindings(self):
+        eq = self.assertEqual
+        w = editwin
+        # Save original text and recreate an empty version.  It is not
+        # actually empty because Text widgets are created with default
+        # events.
+        orig_text = w.text
+        # Multicall has its own versions of the event_* methods.
+        text = w.text = MultiCallCreator(tk.Text)(root)
+
+        keydefs = {'<<zoom-height>>': ['<Alt-Key-9>'],
+                   '<<open-module>>': ['<Control-Shift-O>'],
+                   '<<cancel>>': ['<Cancel>'],
+                   '<<empty>>': [],
+                   '<<indent>>': ['<Alt-bracketleft>'],
+                   '<<copy>>': ['<Control-j>', '<Control-c>']}
+
+        w.apply_bindings(keydefs)
+        eq(text.keydefs, keydefs)
+        # Multicall event_add() formats the key sequences.
+        eq(text.event_info('<<zoom-height>>'), ('<Alt-KeyPress-9>',))
+        eq(text.event_info('<<copy>>'), ('<Control-Key-j>', '<Control-Key-c>'))
+        eq(text.event_info('<<cancel>>'), ('<Key-Cancel>',))
+        # Although apply_bindings() skips events with no keys, Multicall
+        # event_info() just returns an empty tuple for undefined events.
+        eq(text.event_info('<<empty>>'), ())
+        # Not in keydefs.
+        eq(text.event_info('<<python-docs>>'), ())
+
+        # Cleanup.
+        for event, keylist in keydefs.items():
+            text.event_delete(event, *keylist)
+
+        # Use default.
+        w.apply_bindings()
+        eq(text.event_info('<<python-docs>>'), ('<KeyPress-F1>',))
+
+        del w.text
+        w.text = orig_text
+
+
+class ReloadTests(unittest.TestCase):
+    """Test functions called from configdialog for reloading attributes."""
+
+    @classmethod
+    def setUpClass(cls):
+        cls.keydefs = {'<<copy>>': ['<Control-c>', '<Control-C>'],
+                       '<<beginning-of-line>>': ['<Control-a>', '<Home>'],
+                       '<<close-window>>': ['<Alt-F4>'],
+                       '<<python-docs>>': ['<F15>'],
+                       '<<python-context-help>>': ['<Shift-F1>'], }
+        cls.extensions = {'<<zzdummy>>'}
+        cls.ext_keydefs = {'<<zzdummy>>': ['<Alt-Control-Shift-z>']}
+
+    @classmethod
+    def tearDownClass(cls):
+        del cls.keydefs, cls.extensions, cls.ext_keydefs
+
+    def setUp(self):
+        self.save_text = editwin.text
+        editwin.text = MultiCallCreator(tk.Text)(root)
+
+    def tearDown(self):
+        del editwin.text
+        editwin.text = self.save_text
+
+    @mock.patch.object(editor.idleConf, 'GetExtensionBindings')
+    @mock.patch.object(editor.idleConf, 'GetExtensions')
+    @mock.patch.object(editor.idleConf, 'GetCurrentKeySet')
+    def test_RemoveKeyBindings(self, mock_keyset, mock_ext, mock_ext_bindings):
+        eq = self.assertEqual
+        w = editwin
+        tei = w.text.event_info
+        keys = self.keydefs
+        extkeys = self.ext_keydefs
+
+        mock_keyset.return_value = keys
+        mock_ext.return_value = self.extensions
+        mock_ext_bindings.return_value = extkeys
+
+        w.apply_bindings(keys)
+        w.apply_bindings({'<<spam>>': ['<F18>']})
+        w.apply_bindings(extkeys)
+
+        # Bindings exist.
+        for event in keys:
+            self.assertNotEqual(tei(event), ())
+        self.assertNotEqual(tei('<<spam>>'), ())
+        # Extention bindings exist.
+        for event in extkeys:
+            self.assertNotEqual(tei(event), ())
+
+        w.RemoveKeybindings()
+        # Binding events have been deleted.
+        for event in keys:
+            eq(tei(event), ())
+        # Extention bindings have been removed.
+        for event in extkeys:
+            eq(tei(event), ())
+        # Extra keybindings are not removed - only removes those in idleConf.
+        self.assertNotEqual(tei('<<spam>>'), ())
+        # Remove it.
+        w.text.event_delete('<<spam>>', ['<F18>'])
+
+    @mock.patch.object(editor.idleConf, 'GetExtensionBindings')
+    @mock.patch.object(editor.idleConf, 'GetExtensions')
+    @mock.patch.object(editor.idleConf, 'GetCurrentKeySet')
+    def test_ApplyKeyBindings(self, mock_keyset, mock_ext, mock_ext_bindings):
+        eq = self.assertEqual
+        w = editwin
+        tei = w.text.event_info
+        keys = self.keydefs
+        extkeys = self.ext_keydefs
+
+        mock_keyset.return_value = keys
+        mock_ext.return_value = self.extensions
+        mock_ext_bindings.return_value = extkeys
+
+        # Bindings don't exist.
+        for event in keys:
+            eq(tei(event), ())
+        # Extention bindings don't exist.
+        for event in extkeys:
+            eq(tei(event), ())
+
+        w.ApplyKeybindings()
+        eq(tei('<<python-docs>>'), ('<Key-F15>',))
+        eq(tei('<<beginning-of-line>>'), ('<Control-Key-a>', '<Key-Home>'))
+        eq(tei('<<zzdummy>>'), ('<Control-Shift-Alt-Key-z>',))
+        # Check menu accelerator update.
+        eq(w.menudict['help'].entrycget(3, 'accelerator'), 'F15')
+
+        # Calling ApplyBindings is additive.
+        mock_keyset.return_value = {'<<python-docs>>': ['<Shift-F1>']}
+        w.ApplyKeybindings()
+        eq(tei('<<python-docs>>'), ('<Key-F15>', '<Shift-Key-F1>'))
+        w.text.event_delete('<<python-docs>>', ['<Shift-Key-F1>'])
+
+        mock_keyset.return_value = keys
+        w.RemoveKeybindings()
+
+    def test_ResetColorizer(self):
+        pass
+
+    @mock.patch.object(editor.idleConf, 'GetFont')
+    def test_ResetFont(self, mock_getfont):
+        mock_getfont.return_value = ('spam', 16, 'bold')
+        self.assertNotEqual(editwin.text['font'], 'spam 16 bold')
+        editwin.ResetFont()
+        self.assertEqual(editwin.text['font'], 'spam 16 bold')
+
+    @mock.patch.object(editor.idleConf, 'GetOption')
+    def test_set_notabs_indentwidth(self, mock_get_option):
+        save_usetabs = editwin.usetabs
+        save_indentwidth = editwin.indentwidth
+        mock_get_option.return_value = 11
+
+        editwin.usetabs = True
+        editwin.set_notabs_indentwidth()
+        self.assertNotEqual(editwin.indentwidth, 11)
+
+        editwin.usetabs = False
+        editwin.set_notabs_indentwidth()
+        self.assertEqual(editwin.indentwidth, 11)
+
+        editwin.usetabs = save_usetabs
+        editwin.indentwidth = save_indentwidth
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)

From d047c395dce32cd6dc126c773a9190c2315cb5d0 Mon Sep 17 00:00:00 2001
From: Cheryl Sabella <cheryl.sabella@gmail.com>
Date: Wed, 20 Sep 2017 09:07:26 -0400
Subject: [PATCH 2/9] Add blurb

---
 Misc/NEWS.d/next/IDLE/2017-09-20-09-07-09.bpo-31529.w8ioyr.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 Misc/NEWS.d/next/IDLE/2017-09-20-09-07-09.bpo-31529.w8ioyr.rst

diff --git a/Misc/NEWS.d/next/IDLE/2017-09-20-09-07-09.bpo-31529.w8ioyr.rst b/Misc/NEWS.d/next/IDLE/2017-09-20-09-07-09.bpo-31529.w8ioyr.rst
new file mode 100644
index 00000000000000..3fc798866fd2ec
--- /dev/null
+++ b/Misc/NEWS.d/next/IDLE/2017-09-20-09-07-09.bpo-31529.w8ioyr.rst
@@ -0,0 +1 @@
+IDLE: Add docstrings and unittests for some functions in editor.py.

From ac8dae41d2f878c70fc67eb86254af0f5a0ece27 Mon Sep 17 00:00:00 2001
From: Terry Jan Reedy <tjreedy@udel.edu>
Date: Sun, 20 Sep 2020 21:13:35 -0400
Subject: [PATCH 3/9] Synchronize tkinter import in test_editor

---
 Lib/idlelib/idle_test/test_editor.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py
index cc8126fa99af5e..4773c90cc9fe9d 100644
--- a/Lib/idlelib/idle_test/test_editor.py
+++ b/Lib/idlelib/idle_test/test_editor.py
@@ -7,7 +7,7 @@
 from test.support import requires
 import unittest
 from unittest import mock
-from tkinter import Tk
+import tkinter as tk
 from idlelib.multicall import MultiCallCreator
 from idlelib.idle_test.mock_idle import Func
 
@@ -47,7 +47,7 @@ class EditorWindowTest(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         requires('gui')
-        cls.root = Tk()
+        cls.root = tk.Tk()
         cls.root.withdraw()
 
     @classmethod

From eb251409a20434105b2e2ae90308ac602c6948e9 Mon Sep 17 00:00:00 2001
From: Terry Jan Reedy <tjreedy@udel.edu>
Date: Sun, 20 Sep 2020 21:37:47 -0400
Subject: [PATCH 4/9] Update test_editor.py

More tk. prefixes.
---
 Lib/idlelib/idle_test/test_editor.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py
index 4773c90cc9fe9d..24ddff5809fd36 100644
--- a/Lib/idlelib/idle_test/test_editor.py
+++ b/Lib/idlelib/idle_test/test_editor.py
@@ -136,7 +136,7 @@ class IndentAndNewlineTest(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         requires('gui')
-        cls.root = Tk()
+        cls.root = tk.Tk()
         cls.root.withdraw()
         cls.window = Editor(root=cls.root)
         cls.window.indentwidth = 2
@@ -227,7 +227,7 @@ class RMenuTest(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         requires('gui')
-        cls.root = Tk()
+        cls.root = tk.Tk()
         cls.root.withdraw()
         cls.window = Editor(root=cls.root)
 

From 8dfbd49b3a5a9ab338f020aab9a5d6377e7ad6f6 Mon Sep 17 00:00:00 2001
From: Terry Jan Reedy <tjreedy@udel.edu>
Date: Sun, 20 Sep 2020 21:46:54 -0400
Subject: [PATCH 5/9] Remove test of removed _filename_to_unicode method.

---
 Lib/idlelib/idle_test/test_editor.py | 9 ---------
 1 file changed, 9 deletions(-)

diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py
index 24ddff5809fd36..e276fe8ba5c725 100644
--- a/Lib/idlelib/idle_test/test_editor.py
+++ b/Lib/idlelib/idle_test/test_editor.py
@@ -33,15 +33,6 @@ def tearDownModule():
     del root
 
 
-class Editor_func_test(unittest.TestCase):
-    def test_filename_to_unicode(self):
-        func = editor.EditorWindow._filename_to_unicode
-        class dummy(): filesystemencoding = 'utf-8'
-        pairs = (('abc', 'abc'), ('a\U00011111c', 'a\ufffdc'),
-                 (b'abc', 'abc'), (b'a\xf0\x91\x84\x91c', 'a\ufffdc'))
-        for inp, out in pairs:
-            self.assertEqual(func(dummy, inp), out)
-
 class EditorWindowTest(unittest.TestCase):
 
     @classmethod

From 0308f80548ee65676ebff5a8a50f0df89dc8fe52 Mon Sep 17 00:00:00 2001
From: Terry Jan Reedy <tjreedy@udel.edu>
Date: Sun, 20 Sep 2020 23:44:35 -0400
Subject: [PATCH 6/9] Remove unneeded extra underscore.

---
 Lib/idlelib/editor.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py
index cf1d65b3580da0..ae2c1bc3c0c845 100644
--- a/Lib/idlelib/editor.py
+++ b/Lib/idlelib/editor.py
@@ -444,7 +444,6 @@ def set_line_and_column(self, event=None):
         ("help", "_Help"),
     ]
 
-
     def createmenubar(self):
         """Populate the menu bar widget for the editor window.
 
@@ -944,12 +943,12 @@ def reset_help_menu_entries(self):
         if help_list:
             helpmenu.add_separator()
             for entry in help_list:
-                cmd = self.__extra_help_callback(entry[1])
+                cmd = self._extra_help_callback(entry[1])
                 helpmenu.add_command(label=entry[0], command=cmd)
         # And update the menu dictionary.
         self.menudict['help'] = helpmenu
 
-    def __extra_help_callback(self, helpfile):
+    def _extra_help_callback(self, helpfile):
         """Create a callback with the helpfile value frozen at definition time.
 
         Args:

From c32379be1f998e608162a176171989e4f5081c5c Mon Sep 17 00:00:00 2001
From: Terry Jan Reedy <tjreedy@udel.edu>
Date: Mon, 21 Sep 2020 00:16:11 -0400
Subject: [PATCH 7/9] Make test pass; delete redundant asserts.

---
 Lib/idlelib/idle_test/test_editor.py | 24 +++++++++---------------
 1 file changed, 9 insertions(+), 15 deletions(-)

diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py
index e276fe8ba5c725..454e60ae05cc3c 100644
--- a/Lib/idlelib/idle_test/test_editor.py
+++ b/Lib/idlelib/idle_test/test_editor.py
@@ -381,7 +381,7 @@ def test_fill_menus(self):
     def test_reset_help_menu_entries(self, mock_extrahelp):
         w = self.mock_editwin
         mock_extrahelp.return_value = [('Python', 'https://python.org', '1')]
-        mock_callback = w._EditorWindow__extra_help_callback
+        mock_callback = w._extra_help_callback
 
         # Create help menu.
         help = w.menudict['help'] = tk.Menu(w.menubar, name='help', tearoff=False)
@@ -435,24 +435,18 @@ def test__extra_help_callback_not_windows(self, mock_openfile):
 
     @mock.patch.object(editor.os, 'startfile')
     @unittest.skipIf(not sys.platform.startswith('win'), 'this is test for windows system')
-    def test__extra_help_callback_windows(self, mock_openfile):
+    def test_extra_help_callback_windows(self, mock_start):
         # os.startfile doesn't exist on other platforms.
         w = self.mock_editwin
-        ehc = partial(w._EditorWindow__extra_help_callback, w)
-
+        w.showerror = mock.Mock()
+        def ehc(source):
+            return Editor._extra_help_callback(w, source)
         ehc('http://python.org')
-        mock_openfile.called_with('http://python.org')
-        ehc('www.python.org')
-        mock_openfile.called_with('www.python.org')
-        # Filename that opens successfully.
-        mock_openfile.return_value = True
-        ehc('/foo/bar/baz/')
-        mock_openfile.called_with('\\foo\\bar\\baz')
+        mock_start.called_with('http://python.org')
         # Filename that doesn't open.
-        mock_openfile.return_value = False
-        with self.assertRaises(OSError):
-            ehc('/foo/bar/baz/')
-        self.assertEqual(w.showerror.title, 'Document Start Failure')
+        mock_start.side_effect = OSError('boom')
+        ehc('/foo/bar/baz/')()
+        self.assertTrue(w.showerror.callargs.kwargs)
 
 
 class BindingsTest(unittest.TestCase):

From ba274a4247b369a22b69470d51b108f68dfd9cb1 Mon Sep 17 00:00:00 2001
From: Terry Jan Reedy <tjreedy@udel.edu>
Date: Mon, 21 Sep 2020 01:51:41 -0400
Subject: [PATCH 8/9] Docstring and comment and a couple code revisions.

---
 Lib/idlelib/editor.py | 112 ++++++++++++++++--------------------------
 1 file changed, 43 insertions(+), 69 deletions(-)

diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py
index ae2c1bc3c0c845..427aaae4d81e83 100644
--- a/Lib/idlelib/editor.py
+++ b/Lib/idlelib/editor.py
@@ -434,6 +434,26 @@ def set_line_and_column(self, event=None):
         self.status_bar.set_label('column', 'Col: %s' % column)
         self.status_bar.set_label('line', 'Ln: %s' % line)
 
+
+    """ Menu definitions and functions.
+    * self.menubar - the always visible horizontal menu bar.
+    * mainmenu.menudefs - a list of tuples, one for each menubar item.
+      Each tuple pairs a lower-case name and list of dropdown items.
+      Each item is a name, virtual event pair or None for separator.
+    * mainmenu.default_keydefs - maps events to keys.
+    * text.keydefs - same.
+    * cls.menu_specs - menubar name, titlecase display form pairs
+      with Alt-hotkey indicator.  A subset of menudefs items.
+    * self.menudict - map menu name to dropdown menu.
+    * self.recent_files_menu - 2nd level cascade in the file cascade.
+    * self.wmenu_end - set in __init__ (purpose unclear).
+
+    createmenubar, postwindowsmenu, update_menu_label, update_menu_state,
+    ApplyKeybings (2nd part), reset_help_menu_entries,
+    _extra_help_callback, update_recent_files_list,
+    apply_bindings, fill_menus, (other functions?)
+    """
+
     menu_specs = [
         ("file", "_File"),
         ("edit", "_Edit"),
@@ -480,7 +500,10 @@ def createmenubar(self):
         self.reset_help_menu_entries()
 
     def postwindowsmenu(self):
-        # Only called when Window menu exists
+        """Callback to register window.
+
+        Only called when Window menu exists.
+        """
         menu = self.menudict['window']
         end = menu.index("end")
         if end is None:
@@ -863,12 +886,9 @@ def ResetFont(self):
     def RemoveKeybindings(self):
         """Remove the virtual, configurable keybindings.
 
-        This should be called before the keybindings are applied
-        in ApplyKeyBindings() otherwise the old bindings will still exist.
-        Note: this does not remove the Tk/Tcl keybindings attached to
-        Text widgets by default.
+        Leaves the default Tk Text keybindings.
         """
-        # Called from configdialog.py
+        # Called from configdialog.deactivate_current_config.
         self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
         for event, keylist in keydefs.items():
             self.text.event_delete(event, *keylist)
@@ -881,19 +901,17 @@ def RemoveKeybindings(self):
     def ApplyKeybindings(self):
         """Apply the virtual, configurable keybindings.
 
-        The binding events are attached to self.text.  Also, the
-        menu accelerator keys are updated to match the current
-        configuration.
+        Alse update hotkeys to current keyset.
         """
-        # Called from configdialog.py
+        # Called from configdialog.activate_config_changes.
         self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
         self.apply_bindings()
         for extensionName in self.get_standard_extension_names():
             xkeydefs = idleConf.GetExtensionBindings(extensionName)
             if xkeydefs:
                 self.apply_bindings(xkeydefs)
+
         # Update menu accelerators.
-        # XXX - split into its own function and call it from here?
         menuEventDict = {}
         for menu in self.mainmenu.menudefs:
             menuEventDict[menu[0]] = {}
@@ -928,11 +946,7 @@ def set_notabs_indentwidth(self):
                                                   type='int')
 
     def reset_help_menu_entries(self):
-        """Update the additional help entries on the Help menu.
-
-        First the existing additional help entries are removed from
-        the help menu, then the new help entries are added from idleConf.
-        """
+        """Update the additional help entries on the Help menu."""
         help_list = idleConf.GetAllExtraHelpSourcesList()
         helpmenu = self.menudict['help']
         # First delete the extra help entries, if any.
@@ -948,16 +962,9 @@ def reset_help_menu_entries(self):
         # And update the menu dictionary.
         self.menudict['help'] = helpmenu
 
-    def _extra_help_callback(self, helpfile):
-        """Create a callback with the helpfile value frozen at definition time.
-
-        Args:
-            helpfile: Filename or website to open.
-
-        Returns:
-            Function to open the helpfile.
-        """
-        def display_extra_help(helpfile=helpfile):
+    def _extra_help_callback(self, resource):
+        """Return a callback that loads resource (file or web page)."""
+        def display_extra_help(helpfile=resource):
             if not helpfile.startswith(('www', 'http')):
                 helpfile = os.path.normpath(helpfile)
             if sys.platform[:3] == 'win':
@@ -1120,8 +1127,6 @@ def _close(self):
         if self.color:
             self.color.close()
             self.color = None
-        # Allow code context to close its text.after calls.
-        self.text.unbind('<<toggle-code-context>>')
         self.text = None
         self.tkinter_vars = None
         self.per.close()
@@ -1185,11 +1190,7 @@ def load_extension(self, name):
                     self.text.bind(vevent, getattr(ins, methodname))
 
     def apply_bindings(self, keydefs=None):
-        """Add the event bindings in keydefs to self.text.
-
-        Args:
-            keydefs: Virtual events and keybinding definitions.
-        """
+        """Add events with keys to self.text."""
         if keydefs is None:
             keydefs = self.mainmenu.default_keydefs
         text = self.text
@@ -1199,28 +1200,10 @@ def apply_bindings(self, keydefs=None):
                 text.event_add(event, *keylist)
 
     def fill_menus(self, menudefs=None, keydefs=None):
-        """Add appropriate entries to the menus and submenus.
-
-        The default menudefs and keydefs are loaded from idlelib.mainmenu.
-        Menus that are absent or None in self.menudict are ignored.  The
-        default menu type created for submenus from menudefs is `command`.
-        A submenu item of None results in a `separator` menu type.
-        A submenu name beginning with ! represents a `checkbutton` type.
-
-        The menus are stored in self.menudict.
-
-        Args:
-            menudefs: Menu and submenu names, underlines (shortcuts),
-                and events which is a list of tuples of the form:
-                [(menu1, [(submenu1a, '<<virtual event>>'),
-                          (submenu1b, '<<virtual event>>'), ...]),
-                 (menu2, [(submenu2a, '<<virtual event>>'),
-                          (submenu2b, '<<virtual event>>'), ...]),
-                ]
-            keydefs: Virtual events and keybinding definitions.  Used for
-                the 'accelerator' text on the menu.  Stored as a
-                dictionary of
-                {'<<virtual event>>': ['<binding1>', '<binding2>'],}
+        """Fill in dropdown menus used by this window.
+
+        Items whose name begins with '!' become checkbuttons.
+        Other names indicate commands.  None becomes a separator.
         """
         if menudefs is None:
             menudefs = self.mainmenu.menudefs
@@ -1233,7 +1216,7 @@ def fill_menus(self, menudefs=None, keydefs=None):
             if not menu:
                 continue
             for entry in entrylist:
-                if not entry:
+                if entry is None:
                     menu.add_separator()
                 else:
                     label, eventname = entry
@@ -1269,22 +1252,13 @@ def setvar(self, name, value, vartype=None):
         else:
             raise NameError(name)
 
-    def get_var_obj(self, name, vartype=None):
+    def get_var_obj(self, eventname, vartype=None):
         """Return a tkinter variable instance for the event.
-
-        Cache vars in self.tkinter_vars as {name: Var instance}.
-
-        Args:
-            name: Event name.
-            vartype: Tkinter Var type.
-
-        Returns:
-            Tkinter Var instance.
         """
-        var = self.tkinter_vars.get(name)
+        var = self.tkinter_vars.get(eventname)
         if not var and vartype:
-            # create a Tkinter variable object with self.text as master:
-            self.tkinter_vars[name] = var = vartype(self.text)
+            # Create a Tkinter variable object.
+            self.tkinter_vars[eventname] = var = vartype(self.text)
         return var
 
     # Tk implementations of "virtual text methods" -- each platform

From cf8044e1d6c5f4dc45b66cbd2cd1bb900256c79b Mon Sep 17 00:00:00 2001
From: Terry Jan Reedy <tjreedy@udel.edu>
Date: Fri, 12 May 2023 23:48:58 -0400
Subject: [PATCH 9/9] whitespace

---
 Lib/idlelib/idle_test/test_editor.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py
index e80e61e64dec7c..912de3391b7d06 100644
--- a/Lib/idlelib/idle_test/test_editor.py
+++ b/Lib/idlelib/idle_test/test_editor.py
@@ -444,7 +444,7 @@ def ehc(source):
         #mock_start.side_effect = OSError('boom')
         ehc('/foo/bar/baz/')()
         self.assertTrue(w.showerror.callargs.kwargs)
- 
+
 
 class BindingsTest(unittest.TestCase):