312 lines
12 KiB
Python
312 lines
12 KiB
Python
# -*- 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
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
import inspect, os, sys, gc, traceback, types
|
|
from .debug import printExc
|
|
try:
|
|
from importlib import reload as orig_reload
|
|
except ImportError:
|
|
orig_reload = reload
|
|
|
|
|
|
def reloadAll(prefix=None, debug=False):
|
|
"""Automatically reload all modules whose __file__ begins with *prefix*.
|
|
|
|
Skips reload if the file has not been updated (if .pyc is newer than .py)
|
|
If *prefix* is None, then all loaded modules are checked.
|
|
|
|
Returns a dictionary {moduleName: (reloaded, reason)} describing actions taken
|
|
for each module.
|
|
"""
|
|
failed = []
|
|
changed = []
|
|
ret = {}
|
|
for modName, mod in list(sys.modules.items()):
|
|
if not inspect.ismodule(mod):
|
|
ret[modName] = (False, 'not a module')
|
|
continue
|
|
if modName == '__main__':
|
|
ret[modName] = (False, 'ignored __main__')
|
|
continue
|
|
|
|
# Ignore modules without a __file__ that is .py or .pyc
|
|
if getattr(mod, '__file__', None) is None:
|
|
ret[modName] = (False, 'module has no __file__')
|
|
continue
|
|
|
|
if os.path.splitext(mod.__file__)[1] not in ['.py', '.pyc']:
|
|
ret[modName] = (False, '%s not a .py/pyc file' % str(mod.__file__))
|
|
continue
|
|
|
|
# Ignore if the file name does not start with prefix
|
|
if prefix is not None and mod.__file__[:len(prefix)] != prefix:
|
|
ret[modName] = (False, 'file %s not in prefix %s' % (mod.__file__, prefix))
|
|
continue
|
|
|
|
py = os.path.splitext(mod.__file__)[0] + '.py'
|
|
if py in changed:
|
|
# already processed this module
|
|
continue
|
|
if not os.path.isfile(py):
|
|
# skip modules that lie about their __file__
|
|
ret[modName] = (False, '.py does not exist: %s' % py)
|
|
continue
|
|
|
|
# if source file is newer than cache file, then it needs to be reloaded.
|
|
pyc = getattr(mod, '__cached__', py + 'c')
|
|
if not os.path.isfile(pyc):
|
|
ret[modName] = (False, 'code has no pyc file to compare')
|
|
continue
|
|
|
|
if os.stat(pyc).st_mtime > os.stat(py).st_mtime:
|
|
ret[modName] = (False, 'code has not changed since compile')
|
|
continue
|
|
|
|
# keep track of which modules have changed to ensure that duplicate-import modules get reloaded.
|
|
changed.append(py)
|
|
|
|
try:
|
|
reload(mod, debug=debug)
|
|
ret[modName] = (True, None)
|
|
except Exception as exc:
|
|
printExc("Error while reloading module %s, skipping\n" % mod)
|
|
failed.append(mod.__name__)
|
|
ret[modName] = (False, 'reload failed: %s' % traceback.format_exception_only(type(exc), exc))
|
|
|
|
if len(failed) > 0:
|
|
raise Exception("Some modules failed to reload: %s" % ', '.join(failed))
|
|
|
|
return ret
|
|
|
|
|
|
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 %s" % str(module))
|
|
|
|
## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison
|
|
oldDict = module.__dict__.copy()
|
|
orig_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)
|
|
# don't put this inside updateClass because it is reentrant.
|
|
new.__previous_reload_version__ = old
|
|
|
|
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 hasattr(old, '__kwdefaults'):
|
|
old.__kwdefaults__ = new.__kwdefaults__
|
|
old.__doc__ = new.__doc__
|
|
|
|
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 %s" % 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?)
|
|
newBases = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:]
|
|
try:
|
|
ref.__bases__ = newBases
|
|
except TypeError:
|
|
print(" Error setting bases for class %s" % ref)
|
|
print(" old bases: %s" % repr(ref.__bases__))
|
|
print(" new bases: %s" % repr(newBases))
|
|
raise
|
|
if debug:
|
|
print(" Changed superclass for %s" % safeStr(ref))
|
|
#else:
|
|
#if debug:
|
|
#print " Ignoring reference", type(ref)
|
|
except Exception:
|
|
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.isfunction(oa) or inspect.ismethod(oa):
|
|
# note python2 has unbound methods, whereas python3 just uses plain functions
|
|
try:
|
|
na = getattr(new, attr)
|
|
except AttributeError:
|
|
if debug:
|
|
print(" Skipping method update for %s; new class does not have this attribute" % attr)
|
|
continue
|
|
|
|
ofunc = getattr(oa, '__func__', oa) # in py2 we have to get the __func__ from unbound method,
|
|
nfunc = getattr(na, '__func__', na) # in py3 the attribute IS the function
|
|
|
|
if ofunc is not nfunc:
|
|
depth = updateFunction(ofunc, nfunc, debug)
|
|
if not hasattr(nfunc, '__previous_reload_method__'):
|
|
nfunc.__previous_reload_method__ = oa # important for managing signal connection
|
|
#oa.__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 attr == '__previous_reload_version__':
|
|
continue
|
|
if not hasattr(old, attr):
|
|
if debug:
|
|
print(" Adding missing attribute %s" % 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 Exception:
|
|
try:
|
|
s = repr(obj)
|
|
except Exception:
|
|
s = "<instance of %s at 0x%x>" % (safeStr(type(obj)), id(obj))
|
|
return s
|
|
|
|
|
|
def getPreviousVersion(obj):
|
|
"""Return the previous version of *obj*, or None if this object has not
|
|
been reloaded.
|
|
"""
|
|
if isinstance(obj, type) or inspect.isfunction(obj):
|
|
return getattr(obj, '__previous_reload_version__', None)
|
|
elif inspect.ismethod(obj):
|
|
if obj.__self__ is None:
|
|
# unbound method
|
|
return getattr(obj.__func__, '__previous_reload_method__', None)
|
|
else:
|
|
oldmethod = getattr(obj.__func__, '__previous_reload_method__', None)
|
|
if oldmethod is None:
|
|
return None
|
|
self = obj.__self__
|
|
oldfunc = getattr(oldmethod, '__func__', oldmethod)
|
|
if hasattr(oldmethod, 'im_class'):
|
|
# python 2
|
|
cls = oldmethod.im_class
|
|
return types.MethodType(oldfunc, self, cls)
|
|
else:
|
|
# python 3
|
|
return types.MethodType(oldfunc, self)
|