diff -r ab2e7e51869d Doc/whatsnew/3.6.rst --- a/Doc/whatsnew/3.6.rst Sat Jun 25 22:47:04 2016 +0300 +++ b/Doc/whatsnew/3.6.rst Sat Jun 25 23:35:08 2016 +0300 @@ -391,6 +391,19 @@ telnetlib Stéphane Wirtel in :issue:`25485`). +tkinter +------- + +Added methods :meth:`~tkinter.Variable.trace_add`, +:meth:`~tkinter.Variable.trace_remove` and :meth:`~tkinter.Variable.trace_info` +in the :class:`tkinter.Variable` class. They replace old methods +:meth:`~tkinter.Variable.trace_variable`, :meth:`~tkinter.Variable.trace`, +:meth:`~tkinter.Variable.trace_vdelete` and +:meth:`~tkinter.Variable.trace_vinfo` that use obsolete Tcl commands and might +not work in future versions of Tcl. +(Contributed by Serhiy Storchaka in :issue:`22115`). + + typing ------ diff -r ab2e7e51869d Lib/idlelib/configdialog.py --- a/Lib/idlelib/configdialog.py Sat Jun 25 22:47:04 2016 +0300 +++ b/Lib/idlelib/configdialog.py Sat Jun 25 23:35:08 2016 +0300 @@ -465,24 +465,24 @@ class ConfigDialog(Toplevel): return frame def AttachVarCallbacks(self): - self.fontSize.trace_variable('w', self.VarChanged_font) - self.fontName.trace_variable('w', self.VarChanged_font) - self.fontBold.trace_variable('w', self.VarChanged_font) - self.spaceNum.trace_variable('w', self.VarChanged_spaceNum) - self.colour.trace_variable('w', self.VarChanged_colour) - self.builtinTheme.trace_variable('w', self.VarChanged_builtinTheme) - self.customTheme.trace_variable('w', self.VarChanged_customTheme) - self.themeIsBuiltin.trace_variable('w', self.VarChanged_themeIsBuiltin) - self.highlightTarget.trace_variable('w', self.VarChanged_highlightTarget) - self.keyBinding.trace_variable('w', self.VarChanged_keyBinding) - self.builtinKeys.trace_variable('w', self.VarChanged_builtinKeys) - self.customKeys.trace_variable('w', self.VarChanged_customKeys) - self.keysAreBuiltin.trace_variable('w', self.VarChanged_keysAreBuiltin) - self.winWidth.trace_variable('w', self.VarChanged_winWidth) - self.winHeight.trace_variable('w', self.VarChanged_winHeight) - self.startupEdit.trace_variable('w', self.VarChanged_startupEdit) - self.autoSave.trace_variable('w', self.VarChanged_autoSave) - self.encoding.trace_variable('w', self.VarChanged_encoding) + self.fontSize.trace_add('write', self.VarChanged_font) + self.fontName.trace_add('write', self.VarChanged_font) + self.fontBold.trace_add('write', self.VarChanged_font) + self.spaceNum.trace_add('write', self.VarChanged_spaceNum) + self.colour.trace_add('write', self.VarChanged_colour) + self.builtinTheme.trace_add('write', self.VarChanged_builtinTheme) + self.customTheme.trace_add('write', self.VarChanged_customTheme) + self.themeIsBuiltin.trace_add('write', self.VarChanged_themeIsBuiltin) + self.highlightTarget.trace_add('write', self.VarChanged_highlightTarget) + self.keyBinding.trace_add('write', self.VarChanged_keyBinding) + self.builtinKeys.trace_add('write', self.VarChanged_builtinKeys) + self.customKeys.trace_add('write', self.VarChanged_customKeys) + self.keysAreBuiltin.trace_add('write', self.VarChanged_keysAreBuiltin) + self.winWidth.trace_add('write', self.VarChanged_winWidth) + self.winHeight.trace_add('write', self.VarChanged_winHeight) + self.startupEdit.trace_add('write', self.VarChanged_startupEdit) + self.autoSave.trace_add('write', self.VarChanged_autoSave) + self.encoding.trace_add('write', self.VarChanged_encoding) def remove_var_callbacks(self): "Remove callbacks to prevent memory leaks." @@ -493,7 +493,7 @@ class ConfigDialog(Toplevel): self.keyBinding, self.builtinKeys, self.customKeys, self.keysAreBuiltin, self.winWidth, self.winHeight, self.startupEdit, self.autoSave, self.encoding,): - var.trace_vdelete('w', var.trace_vinfo()[0][1]) + var.trace_remove('write', var.trace_info()[0][1]) def VarChanged_font(self, *params): '''When one font attribute changes, save them all, as they are diff -r ab2e7e51869d Lib/tkinter/__init__.py --- a/Lib/tkinter/__init__.py Sat Jun 25 22:47:04 2016 +0300 +++ b/Lib/tkinter/__init__.py Sat Jun 25 23:35:08 2016 +0300 @@ -343,16 +343,9 @@ class Variable: def get(self): """Return value of variable.""" return self._tk.globalgetvar(self._name) - def trace_variable(self, mode, callback): - """Define a trace callback for the variable. - - MODE is one of "r", "w", "u" for read, write, undefine. - CALLBACK must be a function which is called when - the variable is read, written or undefined. - - Return the name of the callback. - """ - f = CallWrapper(callback, None, self).__call__ + + def _register(self, callback): + f = CallWrapper(callback, None, self._root).__call__ cbname = repr(id(f)) try: callback = callback.__func__ @@ -366,25 +359,99 @@ class Variable: if self._tclCommands is None: self._tclCommands = [] self._tclCommands.append(cbname) + return cbname + + def trace_add(self, mode, callback): + """Define a trace callback for the variable. + + Mode is one of "array", "read", "write", "unset", or a list or tuple + of such strings. + Callback must be a function which is called when the variable is + accessed or modified via the array command, read, written or unset. + + Return the name of the callback. + """ + cbname = self._register(callback) + self._tk.call('trace', 'add', 'variable', + self._name, mode, (cbname,)) + return cbname + + def trace_remove(self, mode, cbname): + """Delete the trace callback for a variable. + + Mode is one of "array", "read", "write", "unset" or a list or tuple + of such strings. Must be same as were specified in trace_add(). + cbname is the name of the callback returned from trace_add(). + """ + self._tk.call('trace', 'remove', 'variable', + self._name, mode, cbname) + for m, ca in self.trace_info(): + if self._tk.splitlist(ca)[0] == cbname: + break + else: + self._tk.deletecommand(cbname) + try: + self._tclCommands.remove(cbname) + except ValueError: + pass + + def trace_info(self): + """Return all trace callback information.""" + splitlist = self._tk.splitlist + return [(splitlist(k), v) for k, v in map(splitlist, + splitlist(self._tk.call('trace', 'info', 'variable', self._name)))] + + def trace_variable(self, mode, callback): + """Define a trace callback for the variable. + + MODE is one of "r", "w", "u" for read, write, undefine. + CALLBACK must be a function which is called when + the variable is read, written or undefined. + + Return the name of the callback. + + This deprecated method wraps a deprecated Tcl method that will + likely be removed in the future. Use trace_add() instead. + """ + # TODO: Add deprecation warning + cbname = self._register(callback) self._tk.call("trace", "variable", self._name, mode, cbname) return cbname + trace = trace_variable + def trace_vdelete(self, mode, cbname): """Delete the trace callback for a variable. MODE is one of "r", "w", "u" for read, write, undefine. CBNAME is the name of the callback returned from trace_variable or trace. + + This deprecated method wraps a deprecated Tcl method that will + likely be removed in the future. Use trace_remove() instead. """ + # TODO: Add deprecation warning self._tk.call("trace", "vdelete", self._name, mode, cbname) - self._tk.deletecommand(cbname) - try: - self._tclCommands.remove(cbname) - except ValueError: - pass + cbname = self._tk.splitlist(cbname)[0] + for m, ca in self.trace_info(): + if self._tk.splitlist(ca)[0] == cbname: + break + else: + self._tk.deletecommand(cbname) + try: + self._tclCommands.remove(cbname) + except ValueError: + pass + def trace_vinfo(self): - """Return all trace callback information.""" + """Return all trace callback information. + + This deprecated method wraps a deprecated Tcl method that will + likely be removed in the future. Use trace_info() instead. + """ + # TODO: Add deprecation warning return [self._tk.split(x) for x in self._tk.splitlist( self._tk.call("trace", "vinfo", self._name))] + def __eq__(self, other): """Comparison for equality (==). diff -r ab2e7e51869d Lib/tkinter/test/test_tkinter/test_variables.py --- a/Lib/tkinter/test/test_tkinter/test_variables.py Sat Jun 25 22:47:04 2016 +0300 +++ b/Lib/tkinter/test/test_tkinter/test_variables.py Sat Jun 25 23:35:08 2016 +0300 @@ -1,5 +1,5 @@ import unittest - +import gc from tkinter import (Variable, StringVar, IntVar, DoubleVar, BooleanVar, Tcl, TclError) @@ -87,6 +87,105 @@ class TestVariable(TestBase): v.set("value") self.assertTrue(v.side_effect) + def test_trace_old(self): + # Old interface + v = Var(self.root) + vname = str(v) + trace = [] + def read_tracer(*args): + trace.append(('read',) + args) + def write_tracer(*args): + trace.append(('write',) + args) + cb1 = v.trace_variable('r', read_tracer) + cb2 = v.trace_variable('wu', write_tracer) + self.assertEqual(sorted(v.trace_vinfo()), [('r', cb1), ('wu', cb2)]) + self.assertEqual(trace, []) + + v.set('spam') + self.assertEqual(trace, [('write', vname, '', 'w')]) + + trace = [] + v.get() + self.assertEqual(trace, [('read', vname, '', 'r')]) + + trace = [] + info = sorted(v.trace_vinfo()) + v.trace_vdelete('w', cb1) # Wrong mode + self.assertEqual(sorted(v.trace_vinfo()), info) + with self.assertRaises(TclError): + v.trace_vdelete('r', 'spam') # Wrong command name + self.assertEqual(sorted(v.trace_vinfo()), info) + v.trace_vdelete('r', (cb1, 43)) # Wrong arguments + self.assertEqual(sorted(v.trace_vinfo()), info) + v.get() + self.assertEqual(trace, [('read', vname, '', 'r')]) + + trace = [] + v.trace_vdelete('r', cb1) + self.assertEqual(v.trace_vinfo(), [('wu', cb2)]) + v.get() + self.assertEqual(trace, []) + + trace = [] + del write_tracer + gc.collect() + v.set('eggs') + self.assertEqual(trace, [('write', vname, '', 'w')]) + + trace = [] + del v + gc.collect() + self.assertEqual(trace, [('write', vname, '', 'u')]) + + def test_trace(self): + v = Var(self.root) + vname = str(v) + trace = [] + def read_tracer(*args): + trace.append(('read',) + args) + def write_tracer(*args): + trace.append(('write',) + args) + tr1 = v.trace_add('read', read_tracer) + tr2 = v.trace_add(['write', 'unset'], write_tracer) + self.assertEqual(sorted(v.trace_info()), [ + (('read',), tr1), + (('write', 'unset'), tr2)]) + self.assertEqual(trace, []) + + v.set('spam') + self.assertEqual(trace, [('write', vname, '', 'write')]) + + trace = [] + v.get() + self.assertEqual(trace, [('read', vname, '', 'read')]) + + trace = [] + info = sorted(v.trace_info()) + v.trace_remove('write', tr1) # Wrong mode + self.assertEqual(sorted(v.trace_info()), info) + with self.assertRaises(TclError): + v.trace_remove('read', 'spam') # Wrong command name + self.assertEqual(sorted(v.trace_info()), info) + v.get() + self.assertEqual(trace, [('read', vname, '', 'read')]) + + trace = [] + v.trace_remove('read', tr1) + self.assertEqual(v.trace_info(), [(('write', 'unset'), tr2)]) + v.get() + self.assertEqual(trace, []) + + trace = [] + del write_tracer + gc.collect() + v.set('eggs') + self.assertEqual(trace, [('write', vname, '', 'write')]) + + trace = [] + del v + gc.collect() + self.assertEqual(trace, [('write', vname, '', 'unset')]) + class TestStringVar(TestBase):