summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorcsabella <cheryl.sabella@gmail.com>2017-07-26 23:09:58 (GMT)
committerTerry Jan Reedy <tjreedy@udel.edu>2017-07-26 23:09:58 (GMT)
commit45bf723c6c591ec56a18dad8150ae89797450d8b (patch)
treeb0ab79d45075f418c2f91dbf0e61142eae9faad6
parent5cff6379797967faabbb834a9eb154c3f0839489 (diff)
downloadcpython-45bf723c6c591ec56a18dad8150ae89797450d8b.zip
cpython-45bf723c6c591ec56a18dad8150ae89797450d8b.tar.gz
cpython-45bf723c6c591ec56a18dad8150ae89797450d8b.tar.bz2
bpo-30853: IDLE: Factor a VarTrace class from configdialog.ConfigDialog. (#2872)
The new class manages pairs of tk Variables and trace callbacks. It is completely covered by new tests.
-rw-r--r--Lib/idlelib/configdialog.py55
-rw-r--r--Lib/idlelib/idle_test/test_configdialog.py94
2 files changed, 147 insertions, 2 deletions
diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py
index 1832e15..f98af46 100644
--- a/Lib/idlelib/configdialog.py
+++ b/Lib/idlelib/configdialog.py
@@ -1846,6 +1846,61 @@ class ConfigDialog(Toplevel):
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 54b2d78..ce02ae4 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 @@ class GeneralTest(unittest.TestCase):
#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)