From 223dd8d7d2a126758f32dd2d1cb5d86e36f39fc8 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 11 Jul 2014 00:16:00 -0400 Subject: #21940: Add unittest for WidgetRedirector. Initial patch by Saimadhav Heblikar. --- Lib/idlelib/WidgetRedirector.py | 31 +++++--- Lib/idlelib/idle_test/mock_idle.py | 5 +- Lib/idlelib/idle_test/test_widgetredir.py | 122 ++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 Lib/idlelib/idle_test/test_widgetredir.py diff --git a/Lib/idlelib/WidgetRedirector.py b/Lib/idlelib/WidgetRedirector.py index e7377d5..b3d7bfa 100644 --- a/Lib/idlelib/WidgetRedirector.py +++ b/Lib/idlelib/WidgetRedirector.py @@ -3,19 +3,21 @@ from tkinter import TclError class WidgetRedirector: """Support for redirecting arbitrary widget subcommands. - Some Tk operations don't normally pass through Tkinter. For example, if a + Some Tk operations don't normally pass through tkinter. For example, if a character is inserted into a Text widget by pressing a key, a default Tk binding to the widget's 'insert' operation is activated, and the Tk library - processes the insert without calling back into Tkinter. + processes the insert without calling back into tkinter. - Although a binding to could be made via Tkinter, what we really want - to do is to hook the Tk 'insert' operation itself. + Although a binding to could be made via tkinter, what we really want + to do is to hook the Tk 'insert' operation itself. For one thing, we want + a text.insert call in idle code to have the same effect as a key press. When a widget is instantiated, a Tcl command is created whose name is the same as the pathname widget._w. This command is used to invoke the various widget operations, e.g. insert (for a Text widget). We are going to hook this command and provide a facility ('register') to intercept the widget - operation. + operation. We will also intercept method calls on the tkinter class + instance that represents the tk widget. In IDLE, WidgetRedirector is used in Percolator to intercept Text commands. The function being registered provides access to the top @@ -64,9 +66,13 @@ class WidgetRedirector: def register(self, operation, function): '''Return OriginalCommand(operation) after registering function. - Registration adds an instance function attribute that masks the - class instance method attribute. If a second function is - registered for the same operation, the first function is replaced. + Registration adds an operation: function pair to ._operations. + It also adds an widget function attribute that masks the tkinter + class instance method. Method masking operates independently + from command dispatch. + + If a second function is registered for the same operation, the + first function is replaced in both places. ''' self._operations[operation] = function setattr(self.widget, operation, function) @@ -80,8 +86,10 @@ class WidgetRedirector: if operation in self._operations: function = self._operations[operation] del self._operations[operation] - if hasattr(self.widget, operation): + try: delattr(self.widget, operation) + except AttributeError: + pass return function else: return None @@ -160,8 +168,7 @@ def _widget_redirector(parent): # htest # if __name__ == "__main__": import unittest -## unittest.main('idlelib.idle_test.test_widgetredir', -## verbosity=2, exit=False) - + unittest.main('idlelib.idle_test.test_widgetredir', + verbosity=2, exit=False) from idlelib.idle_test.htest import run run(_widget_redirector) diff --git a/Lib/idlelib/idle_test/mock_idle.py b/Lib/idlelib/idle_test/mock_idle.py index 9c7e567..1672a34 100644 --- a/Lib/idlelib/idle_test/mock_idle.py +++ b/Lib/idlelib/idle_test/mock_idle.py @@ -26,7 +26,10 @@ class Func: self.called = True self.args = args self.kwds = kwds - return self.result + if isinstance(self.result, BaseException): + raise self.result + else: + return self.result class Editor: diff --git a/Lib/idlelib/idle_test/test_widgetredir.py b/Lib/idlelib/idle_test/test_widgetredir.py new file mode 100644 index 0000000..6440561 --- /dev/null +++ b/Lib/idlelib/idle_test/test_widgetredir.py @@ -0,0 +1,122 @@ +"""Unittest for idlelib.WidgetRedirector + +100% coverage +""" +from test.support import requires +import unittest +from idlelib.idle_test.mock_idle import Func +from tkinter import Tk, Text, TclError +from idlelib.WidgetRedirector import WidgetRedirector + + +class InitCloseTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.tk = Tk() + cls.text = Text(cls.tk) + + @classmethod + def tearDownClass(cls): + cls.text.destroy() + cls.tk.destroy() + del cls.text, cls.tk + + def test_init(self): + redir = WidgetRedirector(self.text) + self.assertEqual(redir.widget, self.text) + self.assertEqual(redir.tk, self.text.tk) + self.assertRaises(TclError, WidgetRedirector, self.text) + redir.close() # restore self.tk, self.text + + def test_close(self): + redir = WidgetRedirector(self.text) + redir.register('insert', Func) + redir.close() + self.assertEqual(redir._operations, {}) + self.assertFalse(hasattr(self.text, 'widget')) + + +class WidgetRedirectorTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.tk = Tk() + cls.text = Text(cls.tk) + + @classmethod + def tearDownClass(cls): + cls.text.destroy() + cls.tk.destroy() + del cls.text, cls.tk + + def setUp(self): + self.redir = WidgetRedirector(self.text) + self.func = Func() + self.orig_insert = self.redir.register('insert', self.func) + self.text.insert('insert', 'asdf') # leaves self.text empty + + def tearDown(self): + self.text.delete('1.0', 'end') + self.redir.close() + + def test_repr(self): # partly for 100% coverage + self.assertIn('Redirector', repr(self.redir)) + self.assertIn('Original', repr(self.orig_insert)) + + def test_register(self): + self.assertEqual(self.text.get('1.0', 'end'), '\n') + self.assertEqual(self.func.args, ('insert', 'asdf')) + self.assertIn('insert', self.redir._operations) + self.assertIn('insert', self.text.__dict__) + self.assertEqual(self.text.insert, self.func) + + def test_original_command(self): + self.assertEqual(self.orig_insert.operation, 'insert') + self.assertEqual(self.orig_insert.tk_call, self.text.tk.call) + self.orig_insert('insert', 'asdf') + self.assertEqual(self.text.get('1.0', 'end'), 'asdf\n') + + def test_unregister(self): + self.assertIsNone(self.redir.unregister('invalid operation name')) + self.assertEqual(self.redir.unregister('insert'), self.func) + self.assertNotIn('insert', self.redir._operations) + self.assertNotIn('insert', self.text.__dict__) + + def test_unregister_no_attribute(self): + del self.text.insert + self.assertEqual(self.redir.unregister('insert'), self.func) + + def test_dispatch_intercept(self): + self.func.__init__(True) + self.assertTrue(self.redir.dispatch('insert', False)) + self.assertFalse(self.func.args[0]) + + def test_dispatch_bypass(self): + self.orig_insert('insert', 'asdf') + # tk.call returns '' where Python would return None + self.assertEqual(self.redir.dispatch('delete', '1.0', 'end'), '') + self.assertEqual(self.text.get('1.0', 'end'), '\n') + + def test_dispatch_error(self): + self.func.__init__(TclError()) + self.assertEqual(self.redir.dispatch('insert', False), '') + self.assertEqual(self.redir.dispatch('invalid'), '') + + def test_command_dispatch(self): + # Test that .__init__ causes redirection of tk calls + # through redir.dispatch + self.tk.call(self.text._w, 'insert', 'hello') + self.assertEqual(self.func.args, ('hello',)) + self.assertEqual(self.text.get('1.0', 'end'), '\n') + # Ensure that called through redir .dispatch and not through + # self.text.insert by having mock raise TclError. + self.func.__init__(TclError()) + self.assertEqual(self.tk.call(self.text._w, 'insert', 'boo'), '') + + + +if __name__ == '__main__': + unittest.main(verbosity=2) -- cgit v0.12