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): pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy