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.
This commit is contained in:
parent
5cff637979
commit
45bf723c6c
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue