Skip to content

Commit 0243bea

Browse files
authored
[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)
1 parent 25de5ba commit 0243bea

File tree

2 files changed

+147
-2
lines changed

2 files changed

+147
-2
lines changed

Lib/idlelib/configdialog.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,6 +1846,61 @@ def save_all_changed_extensions(self):
18461846
self.ext_userCfg.Save()
18471847

18481848

1849+
class VarTrace:
1850+
"""Maintain Tk variables trace state."""
1851+
1852+
def __init__(self):
1853+
"""Store Tk variables and callbacks.
1854+
1855+
untraced: List of tuples (var, callback)
1856+
that do not have the callback attached
1857+
to the Tk var.
1858+
traced: List of tuples (var, callback) where
1859+
that callback has been attached to the var.
1860+
"""
1861+
self.untraced = []
1862+
self.traced = []
1863+
1864+
def add(self, var, callback):
1865+
"""Add (var, callback) tuple to untraced list.
1866+
1867+
Args:
1868+
var: Tk variable instance.
1869+
callback: Function to be used as a callback or
1870+
a tuple with IdleConf values for default
1871+
callback.
1872+
1873+
Return:
1874+
Tk variable instance.
1875+
"""
1876+
if isinstance(callback, tuple):
1877+
callback = self.make_callback(var, callback)
1878+
self.untraced.append((var, callback))
1879+
return var
1880+
1881+
@staticmethod
1882+
def make_callback(var, config):
1883+
"Return default callback function to add values to changes instance."
1884+
def default_callback(*params):
1885+
"Add config values to changes instance."
1886+
changes.add_option(*config, var.get())
1887+
return default_callback
1888+
1889+
def attach(self):
1890+
"Attach callback to all vars that are not traced."
1891+
while self.untraced:
1892+
var, callback = self.untraced.pop()
1893+
var.trace_add('write', callback)
1894+
self.traced.append((var, callback))
1895+
1896+
def detach(self):
1897+
"Remove callback from traced vars."
1898+
while self.traced:
1899+
var, callback = self.traced.pop()
1900+
var.trace_remove('write', var.trace_info()[0][1])
1901+
self.untraced.append((var, callback))
1902+
1903+
18491904
help_common = '''\
18501905
When you click either the Apply or Ok buttons, settings in this
18511906
dialog that are different from IDLE's default are saved in

Lib/idlelib/idle_test/test_configdialog.py

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
Half the class creates dialog, half works with user customizations.
44
Coverage: 46% just by creating dialog, 60% with current tests.
55
"""
6-
from idlelib.configdialog import ConfigDialog, idleConf, changes
6+
from idlelib.configdialog import ConfigDialog, idleConf, changes, VarTrace
77
from test.support import requires
88
requires('gui')
9-
from tkinter import Tk
9+
from tkinter import Tk, IntVar, BooleanVar
1010
import unittest
11+
from unittest import mock
1112
import idlelib.config as config
1213
from idlelib.idle_test.mock_idle import Func
1314

@@ -248,5 +249,94 @@ def test_editor_size(self):
248249
#def test_help_sources(self): pass # TODO
249250

250251

252+
class TestVarTrace(unittest.TestCase):
253+
254+
def setUp(self):
255+
changes.clear()
256+
self.v1 = IntVar(root)
257+
self.v2 = BooleanVar(root)
258+
self.called = 0
259+
self.tracers = VarTrace()
260+
261+
def tearDown(self):
262+
del self.v1, self.v2
263+
264+
def var_changed_increment(self, *params):
265+
self.called += 13
266+
267+
def var_changed_boolean(self, *params):
268+
pass
269+
270+
def test_init(self):
271+
self.assertEqual(self.tracers.untraced, [])
272+
self.assertEqual(self.tracers.traced, [])
273+
274+
def test_add(self):
275+
tr = self.tracers
276+
func = Func()
277+
cb = tr.make_callback = mock.Mock(return_value=func)
278+
279+
v1 = tr.add(self.v1, self.var_changed_increment)
280+
self.assertIsInstance(v1, IntVar)
281+
v2 = tr.add(self.v2, self.var_changed_boolean)
282+
self.assertIsInstance(v2, BooleanVar)
283+
284+
v3 = IntVar(root)
285+
v3 = tr.add(v3, ('main', 'section', 'option'))
286+
cb.assert_called_once()
287+
cb.assert_called_with(v3, ('main', 'section', 'option'))
288+
289+
expected = [(v1, self.var_changed_increment),
290+
(v2, self.var_changed_boolean),
291+
(v3, func)]
292+
self.assertEqual(tr.traced, [])
293+
self.assertEqual(tr.untraced, expected)
294+
295+
del tr.make_callback
296+
297+
def test_make_callback(self):
298+
tr = self.tracers
299+
cb = tr.make_callback(self.v1, ('main', 'section', 'option'))
300+
self.assertTrue(callable(cb))
301+
self.v1.set(42)
302+
# Not attached, so set didn't invoke the callback.
303+
self.assertNotIn('section', changes['main'])
304+
# Invoke callback manually.
305+
cb()
306+
self.assertIn('section', changes['main'])
307+
self.assertEqual(changes['main']['section']['option'], '42')
308+
309+
def test_attach_detach(self):
310+
tr = self.tracers
311+
v1 = tr.add(self.v1, self.var_changed_increment)
312+
v2 = tr.add(self.v2, self.var_changed_boolean)
313+
expected = [(v1, self.var_changed_increment),
314+
(v2, self.var_changed_boolean)]
315+
316+
# Attach callbacks and test call increment.
317+
tr.attach()
318+
self.assertEqual(tr.untraced, [])
319+
self.assertCountEqual(tr.traced, expected)
320+
v1.set(1)
321+
self.assertEqual(v1.get(), 1)
322+
self.assertEqual(self.called, 13)
323+
324+
# Check that only one callback is attached to a variable.
325+
# If more than one callback were attached, then var_changed_increment
326+
# would be called twice and the counter would be 2.
327+
self.called = 0
328+
tr.attach()
329+
v1.set(1)
330+
self.assertEqual(self.called, 13)
331+
332+
# Detach callbacks.
333+
self.called = 0
334+
tr.detach()
335+
self.assertEqual(tr.traced, [])
336+
self.assertCountEqual(tr.untraced, expected)
337+
v1.set(1)
338+
self.assertEqual(self.called, 0)
339+
340+
251341
if __name__ == '__main__':
252342
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)