# -*- 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)