diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index fdb47abf43fb77..912de3391b7d06 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -1,12 +1,37 @@ "Test editor, coverage 35%." from idlelib import editor -import unittest from collections import namedtuple +from functools import partial +import sys from test.support import requires -from tkinter import Tk + +import unittest +from unittest import mock +import tkinter as tk +from idlelib.multicall import MultiCallCreator +from idlelib.idle_test.mock_idle import Func Editor = editor.EditorWindow +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 EditorWindowTest(unittest.TestCase): @@ -14,7 +39,7 @@ class EditorWindowTest(unittest.TestCase): @classmethod def setUpClass(cls): requires('gui') - cls.root = Tk() + cls.root = tk.Tk() cls.root.withdraw() @classmethod @@ -103,7 +128,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 @@ -178,7 +203,8 @@ def test_indent_and_newline_event(self): text.tag_add('sel', '1.17', '1.end') nl(None) # Deletes selected text before adding new line. - eq(get('1.0', 'end'), ' def f1(self, a,\n \n return a + b\n') + eq(get('1.0', 'end'), + ' def f1(self, a,\n \n return a + b\n') class RMenuTest(unittest.TestCase): @@ -186,7 +212,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) @@ -207,5 +233,389 @@ def test_rclick(self): pass +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: it skips open-module shortcut. + mock_cocoa.return_value = True + eq(ga(keydefs, '<<open-module>>'), '') + + +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._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'), 'Unix only') + 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'), 'Windows only') + def test_extra_help_callback_windows(self, mock_start): + # os.startfile doesn't exist on other platforms. + w = self.mock_editwin + w.showerror = mock.Mock() + def ehc(source): + return Editor._extra_help_callback(w, source) + ehc('http://python.org') + # 'called_with' requires spec tjr + #mock_start.called_with('http://python.org') + # Filename that doesn't open. + # But get '(X)' tk box 'Document Start Fa...' that needs OK click. + #mock_start.side_effect = OSError('boom') + ehc('/foo/bar/baz/')() + self.assertTrue(w.showerror.callargs.kwargs) + + +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) 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.