From 5a9bcc359125e8b796929553575143dbc60627af Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 4 Apr 2012 14:49:09 -0400 Subject: [PATCH] Added missing reload.py --- reload.py | 506 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 reload.py diff --git a/reload.py b/reload.py new file mode 100644 index 00000000..e1a8179d --- /dev/null +++ b/reload.py @@ -0,0 +1,506 @@ +# -*- coding: utf-8 -*- +""" +Magic Reload Library +Luke Campagnola 2010 + +Python reload function that actually works (the way you expect it to) + - No re-importing necessary + - Modules can be reloaded in any order + - Replaces functions and methods with their updated code + - Changes instances to use updated classes + - Automatically decides which modules to update by comparing file modification times + +Does NOT: + - re-initialize exting instances, even if __init__ changes + - update references to any module-level objects + ie, this does not reload correctly: + from module import someObject + print someObject + ..but you can use this instead: (this works even for the builtin reload) + import module + print module.someObject +""" + + +import inspect, os, sys, __builtin__, gc, traceback +from debug import printExc + +def reloadAll(prefix=None, debug=False): + """Automatically reload everything whose __file__ begins with prefix. + - Skips reload if the file has not been updated (if .pyc is newer than .py) + - if prefix is None, checks all loaded modules + """ + for modName, mod in sys.modules.items(): ## don't use iteritems; size may change during reload + if not inspect.ismodule(mod): + continue + if modName == '__main__': + continue + + ## Ignore if the file name does not start with prefix + if not hasattr(mod, '__file__') or os.path.splitext(mod.__file__)[1] not in ['.py', '.pyc']: + continue + if prefix is not None and mod.__file__[:len(prefix)] != prefix: + continue + + ## ignore if the .pyc is newer than the .py (or if there is no pyc or py) + py = os.path.splitext(mod.__file__)[0] + '.py' + pyc = py + 'c' + if os.path.isfile(pyc) and os.path.isfile(py) and os.stat(pyc).st_mtime >= os.stat(py).st_mtime: + #if debug: + #print "Ignoring module %s; unchanged" % str(mod) + continue + + try: + reload(mod, debug=debug) + except: + printExc("Error while reloading module %s, skipping\n" % mod) + + +def reload(module, debug=False, lists=False, dicts=False): + """Replacement for the builtin reload function: + - Reloads the module as usual + - Updates all old functions and class methods to use the new code + - Updates all instances of each modified class to use the new class + - Can update lists and dicts, but this is disabled by default + - Requires that class and function names have not changed + """ + if debug: + print "Reloading", module + + ## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison + oldDict = module.__dict__.copy() + __builtin__.reload(module) + newDict = module.__dict__ + + ## Allow modules access to the old dictionary after they reload + if hasattr(module, '__reload__'): + module.__reload__(oldDict) + + ## compare old and new elements from each dict; update where appropriate + for k in oldDict: + old = oldDict[k] + new = newDict.get(k, None) + if old is new or new is None: + continue + + if inspect.isclass(old): + if debug: + print " Updating class %s.%s (0x%x -> 0x%x)" % (module.__name__, k, id(old), id(new)) + updateClass(old, new, debug) + + elif inspect.isfunction(old): + depth = updateFunction(old, new, debug) + if debug: + extra = "" + if depth > 0: + extra = " (and %d previous versions)" % depth + print " Updating function %s.%s%s" % (module.__name__, k, extra) + elif lists and isinstance(old, list): + l = old.len() + old.extend(new) + for i in range(l): + old.pop(0) + elif dicts and isinstance(old, dict): + old.update(new) + for k in old: + if k not in new: + del old[k] + + + +## For functions: +## 1) update the code and defaults to new versions. +## 2) keep a reference to the previous version so ALL versions get updated for every reload +def updateFunction(old, new, debug, depth=0, visited=None): + #if debug and depth > 0: + #print " -> also updating previous version", old, " -> ", new + + old.__code__ = new.__code__ + old.__defaults__ = new.__defaults__ + + if visited is None: + visited = [] + if old in visited: + return + visited.append(old) + + ## finally, update any previous versions still hanging around.. + if hasattr(old, '__previous_reload_version__'): + maxDepth = updateFunction(old.__previous_reload_version__, new, debug, depth=depth+1, visited=visited) + else: + maxDepth = depth + + ## We need to keep a pointer to the previous version so we remember to update BOTH + ## when the next reload comes around. + if depth == 0: + new.__previous_reload_version__ = old + return maxDepth + + + +## For classes: +## 1) find all instances of the old class and set instance.__class__ to the new class +## 2) update all old class methods to use code from the new class methods +def updateClass(old, new, debug): + + ## Track town all instances and subclasses of old + refs = gc.get_referrers(old) + for ref in refs: + try: + if isinstance(ref, old) and ref.__class__ is old: + ref.__class__ = new + if debug: + print " Changed class for", safeStr(ref) + elif inspect.isclass(ref) and issubclass(ref, old) and old in ref.__bases__: + ind = ref.__bases__.index(old) + + ## Does not work: + #ref.__bases__ = ref.__bases__[:ind] + (new,) + ref.__bases__[ind+1:] + ## reason: Even though we change the code on methods, they remain bound + ## to their old classes (changing im_class is not allowed). Instead, + ## we have to update the __bases__ such that this class will be allowed + ## as an argument to older methods. + + ## This seems to work. Is there any reason not to? + ## Note that every time we reload, the class hierarchy becomes more complex. + ## (and I presume this may slow things down?) + ref.__bases__ = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:] + if debug: + print " Changed superclass for", safeStr(ref) + #else: + #if debug: + #print " Ignoring reference", type(ref) + except: + print "Error updating reference (%s) for class change (%s -> %s)" % (safeStr(ref), safeStr(old), safeStr(new)) + raise + + ## update all class methods to use new code. + ## Generally this is not needed since instances already know about the new class, + ## but it fixes a few specific cases (pyqt signals, for one) + for attr in dir(old): + oa = getattr(old, attr) + if inspect.ismethod(oa): + try: + na = getattr(new, attr) + except AttributeError: + if debug: + print " Skipping method update for %s; new class does not have this attribute" % attr + continue + + if hasattr(oa, 'im_func') and hasattr(na, 'im_func') and oa.im_func is not na.im_func: + depth = updateFunction(oa.im_func, na.im_func, debug) + #oa.im_class = new ## bind old method to new class ## not allowed + if debug: + extra = "" + if depth > 0: + extra = " (and %d previous versions)" % depth + print " Updating method %s%s" % (attr, extra) + + ## And copy in new functions that didn't exist previously + for attr in dir(new): + if not hasattr(old, attr): + if debug: + print " Adding missing attribute", attr + setattr(old, attr, getattr(new, attr)) + + ## finally, update any previous versions still hanging around.. + if hasattr(old, '__previous_reload_version__'): + updateClass(old.__previous_reload_version__, new, debug) + + +## It is possible to build classes for which str(obj) just causes an exception. +## Avoid thusly: +def safeStr(obj): + try: + s = str(obj) + except: + try: + s = repr(obj) + except: + s = "" % (safeStr(type(obj)), id(obj)) + return s + + + + + +## Tests: +# write modules to disk, import, then re-write and run again +if __name__ == '__main__': + doQtTest = True + try: + from PyQt4 import QtCore + if not hasattr(QtCore, 'Signal'): + QtCore.Signal = QtCore.pyqtSignal + #app = QtGui.QApplication([]) + class Btn(QtCore.QObject): + sig = QtCore.Signal() + def emit(self): + self.sig.emit() + btn = Btn() + except: + raise + print "Error; skipping Qt tests" + doQtTest = False + + + + import os + if not os.path.isdir('test1'): + os.mkdir('test1') + open('test1/__init__.py', 'w') + modFile1 = "test1/test1.py" + modCode1 = """ +import sys +class A(object): + def __init__(self, msg): + object.__init__(self) + self.msg = msg + def fn(self, pfx = ""): + print pfx+"A class:", self.__class__, id(self.__class__) + print pfx+" %%s: %d" %% self.msg + +class B(A): + def fn(self, pfx=""): + print pfx+"B class:", self.__class__, id(self.__class__) + print pfx+" %%s: %d" %% self.msg + print pfx+" calling superclass.. (%%s)" %% id(A) + A.fn(self, " ") +""" + + modFile2 = "test2.py" + modCode2 = """ +from test1.test1 import A +from test1.test1 import B + +a1 = A("ax1") +b1 = B("bx1") +class C(A): + def __init__(self, msg): + #print "| C init:" + #print "| C.__bases__ = ", map(id, C.__bases__) + #print "| A:", id(A) + #print "| A.__init__ = ", id(A.__init__.im_func), id(A.__init__.im_func.__code__), id(A.__init__.im_class) + A.__init__(self, msg + "(init from C)") + +def fn(): + print "fn: %s" +""" + + open(modFile1, 'w').write(modCode1%(1,1)) + open(modFile2, 'w').write(modCode2%"message 1") + import test1.test1 as test1 + import test2 + print "Test 1 originals:" + A1 = test1.A + B1 = test1.B + a1 = test1.A("a1") + b1 = test1.B("b1") + a1.fn() + b1.fn() + #print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + + from test2 import fn, C + + if doQtTest: + print "Button test before:" + btn.sig.connect(fn) + btn.sig.connect(a1.fn) + btn.emit() + #btn.sig.emit() + print "" + + #print "a1.fn referrers:", sys.getrefcount(a1.fn.im_func), gc.get_referrers(a1.fn.im_func) + + + print "Test2 before reload:" + + fn() + oldfn = fn + test2.a1.fn() + test2.b1.fn() + c1 = test2.C('c1') + c1.fn() + + os.remove(modFile1+'c') + open(modFile1, 'w').write(modCode1%(2,2)) + print "\n----RELOAD test1-----\n" + reloadAll(os.path.abspath(__file__)[:10], debug=True) + + + print "Subclass test:" + c2 = test2.C('c2') + c2.fn() + + + os.remove(modFile2+'c') + open(modFile2, 'w').write(modCode2%"message 2") + print "\n----RELOAD test2-----\n" + reloadAll(os.path.abspath(__file__)[:10], debug=True) + + if doQtTest: + print "Button test after:" + btn.emit() + #btn.sig.emit() + + #print "a1.fn referrers:", sys.getrefcount(a1.fn.im_func), gc.get_referrers(a1.fn.im_func) + + print "Test2 after reload:" + fn() + test2.a1.fn() + test2.b1.fn() + + print "\n==> Test 1 Old instances:" + a1.fn() + b1.fn() + c1.fn() + #print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + print "\n==> Test 1 New instances:" + a2 = test1.A("a2") + b2 = test1.B("b2") + a2.fn() + b2.fn() + c2 = test2.C('c2') + c2.fn() + #print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + + + + os.remove(modFile1+'c') + os.remove(modFile2+'c') + open(modFile1, 'w').write(modCode1%(3,3)) + open(modFile2, 'w').write(modCode2%"message 3") + + print "\n----RELOAD-----\n" + reloadAll(os.path.abspath(__file__)[:10], debug=True) + + if doQtTest: + print "Button test after:" + btn.emit() + #btn.sig.emit() + + #print "a1.fn referrers:", sys.getrefcount(a1.fn.im_func), gc.get_referrers(a1.fn.im_func) + + print "Test2 after reload:" + fn() + test2.a1.fn() + test2.b1.fn() + + print "\n==> Test 1 Old instances:" + a1.fn() + b1.fn() + print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + print "\n==> Test 1 New instances:" + a2 = test1.A("a2") + b2 = test1.B("b2") + a2.fn() + b2.fn() + print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + + os.remove(modFile1) + os.remove(modFile2) + os.remove(modFile1+'c') + os.remove(modFile2+'c') + os.system('rm -r test1') + + + + + + + + +# +# Failure graveyard ahead: +# + + +"""Reload Importer: +Hooks into import system to +1) keep a record of module dependencies as they are imported +2) make sure modules are always reloaded in correct order +3) update old classes and functions to use reloaded code""" + +#import imp, sys + +## python's import hook mechanism doesn't work since we need to be +## informed every time there is an import statement, not just for new imports +#class ReloadImporter: + #def __init__(self): + #self.depth = 0 + + #def find_module(self, name, path): + #print " "*self.depth + "find: ", name, path + ##if name == 'PyQt4' and path is None: + ##print "PyQt4 -> PySide" + ##self.modData = imp.find_module('PySide') + ##return self + ##return None ## return none to allow the import to proceed normally; return self to intercept with load_module + #self.modData = imp.find_module(name, path) + #self.depth += 1 + ##sys.path_importer_cache = {} + #return self + + #def load_module(self, name): + #mod = imp.load_module(name, *self.modData) + #self.depth -= 1 + #print " "*self.depth + "load: ", name + #return mod + +#def pathHook(path): + #print "path hook:", path + #raise ImportError +#sys.path_hooks.append(pathHook) + +#sys.meta_path.append(ReloadImporter()) + + +### replace __import__ with a wrapper that tracks module dependencies +#modDeps = {} +#reloadModule = None +#origImport = __builtins__.__import__ +#def _import(name, globals=None, locals=None, fromlist=None, level=-1, stack=[]): + ### Note that stack behaves as a static variable. + ##print " "*len(importStack) + "import %s" % args[0] + #stack.append(set()) + #mod = origImport(name, globals, locals, fromlist, level) + #deps = stack.pop() + #if len(stack) > 0: + #stack[-1].add(mod) + #elif reloadModule is not None: ## If this is the top level import AND we're inside a module reload + #modDeps[reloadModule].add(mod) + + #if mod in modDeps: + #modDeps[mod] |= deps + #else: + #modDeps[mod] = deps + + + #return mod + +#__builtins__.__import__ = _import + +### replace +#origReload = __builtins__.reload +#def _reload(mod): + #reloadModule = mod + #ret = origReload(mod) + #reloadModule = None + #return ret +#__builtins__.reload = _reload + + +#def reload(mod, visited=None): + #if visited is None: + #visited = set() + #if mod in visited: + #return + #visited.add(mod) + #for dep in modDeps.get(mod, []): + #reload(dep, visited) + #__builtins__.reload(mod)