--- Tkinter-orig.py Wed Jul 19 11:07:10 2006 +++ Tkinter-leak2.py Wed Jul 19 11:20:26 2006 @@ -38,6 +38,8 @@ import _tkinter # If this fails your Python may not be configured for Tk tkinter = _tkinter # b/w compat for export TclError = _tkinter.TclError +if not globals().has_key('TkinterError'): + class TkinterError(TclError): 'Exception in Tkinter python code' from types import * from Tkconstants import * try: @@ -168,6 +170,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 +198,12 @@ def __del__(self): """Unset the variable in Tcl.""" self._tk.globalunsetvar(self._name) + if self._tclCommands: + if self._master._tclCommands is None: + self._master._tclCommands = [] + self._master._tclCommands.extend(self._tclCommands) + for cb in self._tclCommands: + self._master.deletecommand(cb) def __str__(self): """Return the name of the variable in Tcl.""" return self._name @@ -214,7 +223,17 @@ Return the name of the callback. """ cbname = self._master._register(callback) - self._tk.call("trace", "variable", self._name, mode, cbname) + try: + self._tk.call("trace", "variable", self._name, mode, cbname) + except TclError: + # Cleanup command to avoid leaks + self._master.deletecommand(cbname) + raise + # Callback was used OK, so move name from master to Variable instance + if self._tclCommands is None: + self._tclCommands = [] + self._master._tclCommands.pop() + self._tclCommands.append(cbname) return cbname trace = trace_variable def trace_vdelete(self, mode, cbname): @@ -225,6 +244,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 +378,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. @@ -502,7 +527,11 @@ except TclError: pass name = self._register(callit) - return self.tk.call('after', ms, name) + try: + return self.tk.call('after', ms, name) + except TclError: + self.deletecommand(name) + raise def after_idle(self, func, *args): """Call FUNC once if the Tcl main loop has no event to process. @@ -932,18 +961,37 @@ self.tk.call('bindtags', self._w)) else: self.tk.call('bindtags', self._w, tagList) + def _bind_names(self, tclcode): + """Internal function.""" + # 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 Tcl code for commands we have generated + 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' % (add and '+' or '', funcid, self._subst_format_str)) - self.tk.call(what + (sequence, cmd)) + try: + self.tk.call(what + (sequence, cmd)) + except TclError: + self.deletecommand(funcid) + raise return funcid elif sequence: return self.tk.call(what + (sequence,)) @@ -982,19 +1030,20 @@ 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) + # Note: The funcid parameter was always flawed - if 'add' was used + # to bind 2 callbacks, then unbind() could not delete both. + # It was used like this: 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 +1052,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 +1069,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) @@ -1050,10 +1105,34 @@ else: cnf = _cnfmerge(cnf) res = () - for k, v in cnf.items(): + # In order to allow partial backing out of created commands, we always + # process the options in order + keys = cnf.keys() + keys.sort() + for k in keys: + v = cnf[k] 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 +1168,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 @@ -1185,7 +1273,40 @@ x = self.tk.split( self.tk.call(_flatten((self._w, cmd, '-'+cnf)))) return (x[0][1:],) + x[1:] - self.tk.call(_flatten((self._w, cmd)) + self._options(cnf)) + olen = len(self._tclCommands or ()) + try: + self.tk.call(_flatten((self._w, cmd)) + self._options(cnf)) + except TclError, e: + # The cleanup process is nasty because any valid options preceding + # the erroneous one will have been applied, and any following ones not applied. + # We work in alphabetical order in _options() and here. + import sys + e = str(e) + if not (e.startswith('unknown option "') and e.endswith('"')): + sys.stderr.write("Warning: configure() memory leak (unknown error %s)\n" % e) + raise + ename = e[16:-1] + # Get number of commands created that need to be deleted + if kw: + cnf = _cnfmerge((cnf, kw)) + elif cnf: + cnf = _cnfmerge(cnf) + keys = cnf.keys() + keys.sort(reverse=True) + count = 0 + for k in keys: + if callable(cnf[k]): + count += 1 + if k == ename: break + # Sanity check + num = len(self._tclCommands or ()) - olen + if num < count: + sys.stderr.write("Warning: configure() memory leak (bad count %s of %s)\n" % (count, num)) + raise + # Cleanup (phew) + for i in xrange(num): + self.deletecommand(self._tclCommands[-1]) + raise # These used to be defined in Widget: def configure(self, cnf=None, **kw): """Configure resources of a widget. @@ -1570,6 +1691,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: