[Python-checkins] [3.6] bpo-30853: IDLE: Factor a VarTrace class from configdialog.ConfigDialog. (GH-2872) (#2903)

Terry Jan Reedy webhook-mailer at python.org
Wed Jul 26 20:53:16 EDT 2017


https://github.com/python/cpython/commit/0243bea55dc340067247e635442f2a227705315a
commit: 0243bea55dc340067247e635442f2a227705315a
branch: 3.6
author: Terry Jan Reedy <tjreedy at udel.edu>
committer: GitHub <noreply at github.com>
date: 2017-07-26T20:53:13-04:00
summary:

[3.6] bpo-30853:  IDLE: Factor a VarTrace class from configdialog.ConfigDialog. (GH-2872) (#2903)

The new class manages pairs of tk Variables and trace callbacks.
It is completely covered by new tests.
(cherry picked from commit 45bf723)

files:
M Lib/idlelib/configdialog.py
M Lib/idlelib/idle_test/test_configdialog.py

diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py
index 1832e156dc6..f98af4600ee 100644
--- a/Lib/idlelib/configdialog.py
+++ b/Lib/idlelib/configdialog.py
@@ -1846,6 +1846,61 @@ def save_all_changed_extensions(self):
             self.ext_userCfg.Save()
 
 
+class VarTrace:
+    """Maintain Tk variables trace state."""
+
+    def __init__(self):
+        """Store Tk variables and callbacks.
+
+        untraced: List of tuples (var, callback)
+            that do not have the callback attached
+            to the Tk var.
+        traced: List of tuples (var, callback) where
+            that callback has been attached to the var.
+        """
+        self.untraced = []
+        self.traced = []
+
+    def add(self, var, callback):
+        """Add (var, callback) tuple to untraced list.
+
+        Args:
+            var: Tk variable instance.
+            callback: Function to be used as a callback or
+                a tuple with IdleConf values for default
+                callback.
+
+        Return:
+            Tk variable instance.
+        """
+        if isinstance(callback, tuple):
+            callback = self.make_callback(var, callback)
+        self.untraced.append((var, callback))
+        return var
+
+    @staticmethod
+    def make_callback(var, config):
+        "Return default callback function to add values to changes instance."
+        def default_callback(*params):
+            "Add config values to changes instance."
+            changes.add_option(*config, var.get())
+        return default_callback
+
+    def attach(self):
+        "Attach callback to all vars that are not traced."
+        while self.untraced:
+            var, callback = self.untraced.pop()
+            var.trace_add('write', callback)
+            self.traced.append((var, callback))
+
+    def detach(self):
+        "Remove callback from traced vars."
+        while self.traced:
+            var, callback = self.traced.pop()
+            var.trace_remove('write', var.trace_info()[0][1])
+            self.untraced.append((var, callback))
+
+
 help_common = '''\
 When you click either the Apply or Ok buttons, settings in this
 dialog that are different from IDLE's default are saved in
diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py
index 54b2d78d667..ce02ae4a8e6 100644
--- a/Lib/idlelib/idle_test/test_configdialog.py
+++ b/Lib/idlelib/idle_test/test_configdialog.py
@@ -3,11 +3,12 @@
 Half the class creates dialog, half works with user customizations.
 Coverage: 46% just by creating dialog, 60% with current tests.
 """
-from idlelib.configdialog import ConfigDialog, idleConf, changes
+from idlelib.configdialog import ConfigDialog, idleConf, changes, VarTrace
 from test.support import requires
 requires('gui')
-from tkinter import Tk
+from tkinter import Tk, IntVar, BooleanVar
 import unittest
+from unittest import mock
 import idlelib.config as config
 from idlelib.idle_test.mock_idle import Func
 
@@ -248,5 +249,94 @@ def test_editor_size(self):
     #def test_help_sources(self): pass  # TODO
 
 
+class TestVarTrace(unittest.TestCase):
+
+    def setUp(self):
+        changes.clear()
+        self.v1 = IntVar(root)
+        self.v2 = BooleanVar(root)
+        self.called = 0
+        self.tracers = VarTrace()
+
+    def tearDown(self):
+        del self.v1, self.v2
+
+    def var_changed_increment(self, *params):
+        self.called += 13
+
+    def var_changed_boolean(self, *params):
+        pass
+
+    def test_init(self):
+        self.assertEqual(self.tracers.untraced, [])
+        self.assertEqual(self.tracers.traced, [])
+
+    def test_add(self):
+        tr = self.tracers
+        func = Func()
+        cb = tr.make_callback = mock.Mock(return_value=func)
+
+        v1 = tr.add(self.v1, self.var_changed_increment)
+        self.assertIsInstance(v1, IntVar)
+        v2 = tr.add(self.v2, self.var_changed_boolean)
+        self.assertIsInstance(v2, BooleanVar)
+
+        v3 = IntVar(root)
+        v3 = tr.add(v3, ('main', 'section', 'option'))
+        cb.assert_called_once()
+        cb.assert_called_with(v3, ('main', 'section', 'option'))
+
+        expected = [(v1, self.var_changed_increment),
+                    (v2, self.var_changed_boolean),
+                    (v3, func)]
+        self.assertEqual(tr.traced, [])
+        self.assertEqual(tr.untraced, expected)
+
+        del tr.make_callback
+
+    def test_make_callback(self):
+        tr = self.tracers
+        cb = tr.make_callback(self.v1, ('main', 'section', 'option'))
+        self.assertTrue(callable(cb))
+        self.v1.set(42)
+        # Not attached, so set didn't invoke the callback.
+        self.assertNotIn('section', changes['main'])
+        # Invoke callback manually.
+        cb()
+        self.assertIn('section', changes['main'])
+        self.assertEqual(changes['main']['section']['option'], '42')
+
+    def test_attach_detach(self):
+        tr = self.tracers
+        v1 = tr.add(self.v1, self.var_changed_increment)
+        v2 = tr.add(self.v2, self.var_changed_boolean)
+        expected = [(v1, self.var_changed_increment),
+                    (v2, self.var_changed_boolean)]
+
+        # Attach callbacks and test call increment.
+        tr.attach()
+        self.assertEqual(tr.untraced, [])
+        self.assertCountEqual(tr.traced, expected)
+        v1.set(1)
+        self.assertEqual(v1.get(), 1)
+        self.assertEqual(self.called, 13)
+
+        # Check that only one callback is attached to a variable.
+        # If more than one callback were attached, then var_changed_increment
+        # would be called twice and the counter would be 2.
+        self.called = 0
+        tr.attach()
+        v1.set(1)
+        self.assertEqual(self.called, 13)
+
+        # Detach callbacks.
+        self.called = 0
+        tr.detach()
+        self.assertEqual(tr.traced, [])
+        self.assertCountEqual(tr.untraced, expected)
+        v1.set(1)
+        self.assertEqual(self.called, 0)
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)



More information about the Python-checkins mailing list