--- Tkinter-orig.py Mon Jul 17 17:07:17 2006 +++ Tkinter-leak.py Tue Jul 18 16:21:30 2006 @@ -38,6 +38,7 @@ import _tkinter # If this fails your Python may not be configured for Tk tkinter = _tkinter # b/w compat for export TclError = _tkinter.TclError +class TkinterError(TclError): 'Exception in Tkinter python code' from types import * from Tkconstants import * try: @@ -168,6 +169,7 @@ Subclasses StringVar, IntVar, DoubleVar, BooleanVar are specializations that constrain the type of the value returned from get().""" _default = "" + _tclCommands = None def __init__(self, master=None, value=None, name=None): """Construct a variable @@ -195,6 +197,8 @@ def __del__(self): """Unset the variable in Tcl.""" self._tk.globalunsetvar(self._name) + for cb in self._tclCommands: + self._master.deletecommand(cb) def __str__(self): """Return the name of the variable in Tcl.""" return self._name @@ -214,6 +218,9 @@ Return the name of the callback. """ cbname = self._master._register(callback) + if self._tclCommands is None: + self._tclCommands = [] + self._tclCommands.append(cbname) self._tk.call("trace", "variable", self._name, mode, cbname) return cbname trace = trace_variable @@ -225,6 +232,7 @@ """ self._tk.call("trace", "vdelete", self._name, mode, cbname) self._master.deletecommand(cbname) + self._tclCommands.remove(cbname) def trace_vinfo(self): """Return all trace callback information.""" return map(self._tk.split, self._tk.splitlist( @@ -358,11 +366,16 @@ Delete the Tcl command provided in NAME.""" #print '- Tkinter: deleted command', name - self.tk.deletecommand(name) - try: + if name in (self._tclCommands or ()): + self.tk.deletecommand(name) self._tclCommands.remove(name) - except ValueError: - pass + else: + root = self._root() + if name in (root._tclCommands or ()): + # Found binding against root instead of widget, allow deletion + root.deletecommand(name) + else: + raise TkinterError, "deletecommand() did not find given callback %r in widget %r or root %r" % (name, self, root) def tk_strictMotif(self, boolean=None): """Set Tcl internal variable, whether the look and feel should adhere to Motif. @@ -932,11 +945,28 @@ self.tk.call('bindtags', self._w)) else: self.tk.call('bindtags', self._w, tagList) + + # Extract the command names from that returned by bind(sequence) + # bind(seq, func) returns an id, bind(seq) returns tcl source, + # and it gets a bit more interesting if you call bind(..., add=1) + # So, scour the tclcode for commands we have generated + def _bind_names(self, tclcode): + """Internal function.""" + ret = [] + if not tclcode: return ret + for n in tclcode.split("PYCMD")[1:]: + ret.append("PYCMD" + n.split(None, 1)[0]) + return ret + def _bind(self, what, sequence, func, add, needcleanup=1): """Internal function.""" if type(func) is StringType: self.tk.call(what + (sequence, func)) elif func: + if not add: + # Clean out slot before over-writing + for cb in self._bind_names(self._bind(what, sequence, None, None)): + self.deletecommand(cb) funcid = self._register(func, self._substitute, needcleanup) cmd = ('%sif {"[%s %s]" == "break"} break\n' @@ -982,19 +1012,21 @@ be called additionally to the other bound function or whether it will replace the previous function. - Bind will return an identifier to allow deletion of the bound function with - unbind without memory leak. - If FUNC or SEQUENCE is omitted the bound function or list of bound events are returned.""" return self._bind(('bind', self._w), sequence, func, add) def unbind(self, sequence, funcid=None): - """Unbind for this widget for event SEQUENCE the - function identified with FUNCID.""" + """Unbind all callbacks for this widget for event SEQUENCE. + The FUNCID parameter is no longer used.""" + cbs = self._bind_names(self._bind(('bind', self._w), sequence, func=None, add=None)) self.tk.call('bind', self._w, sequence, '') - if funcid: - self.deletecommand(funcid) + for cb in cbs: + self.deletecommand(cb) + # The funcid parameter was always flawed - if 'add' was used + # to bind 2 callbacks, then unbind() could not delete both. + #if funcid: + # self.deletecommand(funcid) def bind_all(self, sequence=None, func=None, add=None): """Bind to all widgets at an event SEQUENCE a call to function FUNC. An additional boolean parameter ADD specifies whether FUNC will @@ -1003,7 +1035,10 @@ return self._bind(('bind', 'all'), sequence, func, add, 0) def unbind_all(self, sequence): """Unbind for all widgets for event SEQUENCE all functions.""" + cbs = self._bind_names(self._bind(('bind', 'all'), sequence, func=None, add=None)) self.tk.call('bind', 'all' , sequence, '') + for cb in cbs: + self.deletecommand(cb) def bind_class(self, className, sequence=None, func=None, add=None): """Bind to widgets with bindtag CLASSNAME at event @@ -1017,7 +1052,10 @@ def unbind_class(self, className, sequence): """Unbind for a all widgets with bindtag CLASSNAME for event SEQUENCE all functions.""" + cbs = self._bind_names(self._bind(('bind', className), sequence, func=None, add=None)) self.tk.call('bind', className , sequence, '') + for cb in cbs: + self.deletecommand(cb) def mainloop(self, n=0): """Call the mainloop of Tk.""" self.tk.mainloop(n) @@ -1054,6 +1092,25 @@ if v is not None: if k[-1] == '_': k = k[:-1] if callable(v): + # If we are replacing a callback, delete it first + try: + # If you call ['command'] immediately after configuring + # it, it will return a string. + # After the button has been invoked, however it then + # returns a _tkinter.Tcl_Obj instance. + # Python 2.1, 2.2 always return string, + # but Python 2.3, 2.4, 2.5 are bugged. + # We pass through str() to always get the right thing. + cb = str(self.cget(k)) + except TclError: + # If trying to configure with an unsupported option, + # ignore exception here in order to generate a + # more meaningful exception from configure call + pass + else: + if cb: + # Prevent memory leak + self.deletecommand(cb) v = self._register(v) res = res + ('-'+k, v) return res @@ -1089,11 +1146,20 @@ name = name + func.__name__ except AttributeError: pass + # Make names easy to spot in Tcl source - see _bind_names() + name = "PYCMD" + name self.tk.createcommand(name, f) if needcleanup: if self._tclCommands is None: self._tclCommands = [] self._tclCommands.append(name) + else: + # If 'needcleanup' is false, it still needs cleanup! So register + # against the root window, i.e. associate with interpreter instance + root = self._root() + if root._tclCommands is None: + root._tclCommands = [] + root._tclCommands.append(name) #print '+ Tkinter created command', name return name register = _register @@ -1570,6 +1636,12 @@ """Bind function FUNC to command NAME for this widget. Return the function bound to NAME if None is given. NAME could be e.g. "WM_SAVE_YOURSELF" or "WM_DELETE_WINDOW".""" + # It seems that to "unbind" a callback, you have to set func to '', + # so cleanup in this case also + if callable(func) or func == '': + cb = self.wm_protocol(name) + if cb: + self.deletecommand(cb) if callable(func): command = self._register(func) else: