From c21d67984c02795e885704e8ca2cfb19b26f63c5 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella 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('<>') 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, '<>'), + (submenu1b, '<>'), ...]), + (menu2, [(submenu2a, '<>'), + (submenu2b, '<>'), ...]), + ] + keydefs: Virtual events and keybinding definitions. Used for + the 'accelerator' text on the menu. Stored as a + dictionary of + {'<>': ['', ''],} """ 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): "<>"}): 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 = {'<>': [''], + '<>': [''], + '<>': [''], + '<>': [''], + '<>': ['', '']} + + mock_cocoa.return_value = False + eq(ga(keydefs, '<>'), '') # Not in keydefs. + eq(ga(keydefs, '<>'), 'Ctrl+J') # Control to Ctrl and first only. + eq(ga(keydefs, '<>'), 'Alt+9') # Remove Key-. + eq(ga(keydefs, '<>'), 'Alt+[') # bracketleft to [. + eq(ga(keydefs, '<>'), 'Ctrl+Break') # Cancel to Ctrl-Break. + eq(ga(keydefs, '<>'), 'Ctrl+Shift+O') # Shift doesn't change. + + # Cocoa test. + mock_cocoa.return_value = True + eq(ga(keydefs, '<>'), '') # 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', '<>'), + None, + ('!Deb_ug', '<>')]), + ('shell', [('_View', '<>'), ]), + ('windows', [('Zoom Height', '<>')]), ] + keydefs = {'<>': ['']} + + 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('<>', 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('<>')) + self.assertNotIn('<>', w.tkinter_vars) + + # Create BooleanVar. + self.assertIsInstance(gvo('<>', tk.BooleanVar), + tk.BooleanVar) + self.assertIn('<>', w.tkinter_vars) + + # No vartype - check cache. + self.assertIsInstance(gvo('<>'), 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 = {'<>': [''], + '<>': [''], + '<>': [''], + '<>': [], + '<>': [''], + '<>': ['', '']} + + w.apply_bindings(keydefs) + eq(text.keydefs, keydefs) + # Multicall event_add() formats the key sequences. + eq(text.event_info('<>'), ('',)) + eq(text.event_info('<>'), ('', '')) + eq(text.event_info('<>'), ('',)) + # Although apply_bindings() skips events with no keys, Multicall + # event_info() just returns an empty tuple for undefined events. + eq(text.event_info('<>'), ()) + # Not in keydefs. + eq(text.event_info('<>'), ()) + + # Cleanup. + for event, keylist in keydefs.items(): + text.event_delete(event, *keylist) + + # Use default. + w.apply_bindings() + eq(text.event_info('<>'), ('',)) + + 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 = {'<>': ['', ''], + '<>': ['', ''], + '<>': [''], + '<>': [''], + '<>': [''], } + cls.extensions = {'<>'} + cls.ext_keydefs = {'<>': ['']} + + @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({'<>': ['']}) + w.apply_bindings(extkeys) + + # Bindings exist. + for event in keys: + self.assertNotEqual(tei(event), ()) + self.assertNotEqual(tei('<>'), ()) + # 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('<>'), ()) + # Remove it. + w.text.event_delete('<>', ['']) + + @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('<>'), ('',)) + eq(tei('<>'), ('', '')) + eq(tei('<>'), ('',)) + # Check menu accelerator update. + eq(w.menudict['help'].entrycget(3, 'accelerator'), 'F15') + + # Calling ApplyBindings is additive. + mock_keyset.return_value = {'<>': ['']} + w.ApplyKeybindings() + eq(tei('<>'), ('', '')) + w.text.event_delete('<>', ['']) + + 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 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 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 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 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 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 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 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('<>') 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, '<>'), - (submenu1b, '<>'), ...]), - (menu2, [(submenu2a, '<>'), - (submenu2b, '<>'), ...]), - ] - keydefs: Virtual events and keybinding definitions. Used for - the 'accelerator' text on the menu. Stored as a - dictionary of - {'<>': ['', ''],} + """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 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):