diff options
author | csabella <cheryl.sabella@gmail.com> | 2017-07-26 23:09:58 (GMT) |
---|---|---|
committer | Terry Jan Reedy <tjreedy@udel.edu> | 2017-07-26 23:09:58 (GMT) |
commit | 45bf723c6c591ec56a18dad8150ae89797450d8b (patch) | |
tree | b0ab79d45075f418c2f91dbf0e61142eae9faad6 | |
parent | 5cff6379797967faabbb834a9eb154c3f0839489 (diff) | |
download | cpython-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.py | 55 | ||||
-rw-r--r-- | Lib/idlelib/idle_test/test_configdialog.py | 94 |
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) |