You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
207 lines
6.9 KiB
207 lines
6.9 KiB
"""Alternative to reload(). |
|
|
|
This works by executing the module in a scratch namespace, and then |
|
patching classes, methods and functions in place. This avoids the |
|
need to patch instances. New objects are copied into the target |
|
namespace. |
|
|
|
Some of the many limitiations include: |
|
|
|
- Global mutable objects other than classes are simply replaced, not patched |
|
|
|
- Code using metaclasses is not handled correctly |
|
|
|
- Code creating global singletons is not handled correctly |
|
|
|
- Functions and methods using decorators (other than classmethod and |
|
staticmethod) is not handled correctly |
|
|
|
- Renamings are not handled correctly |
|
|
|
- Dependent modules are not reloaded |
|
|
|
- When a dependent module contains 'from foo import bar', and |
|
reloading foo deletes foo.bar, the dependent module continues to use |
|
the old foo.bar object rather than failing |
|
|
|
- Frozen modules and modules loaded from zip files aren't handled |
|
correctly |
|
|
|
- Classes involving __slots__ are not handled correctly |
|
""" |
|
|
|
import imp |
|
import sys |
|
import types |
|
|
|
|
|
def xreload(mod, path=None): |
|
"""Reload a module in place, updating classes, methods and functions. |
|
|
|
mod can live in a package. If path is None, we try to find mod from mod's |
|
packages's path. If path is not None, we try to use it as the list of top |
|
level packages for mod. For example: |
|
|
|
mod = xxx.yyy.m. If path is None, we try to find m in xxx.yyy.__path__. |
|
If path is ['path1/', 'path2/'], we try to find m in 'path1/xxx/yyy' first |
|
and then in 'path2/xxx/yyy'. |
|
|
|
Args: |
|
mod: a module object |
|
|
|
Returns: |
|
The (updated) input object itself. |
|
""" |
|
# Get the module name, e.g. 'foo.bar.whatever' |
|
modname = mod.__name__ |
|
# Get the module namespace (dict) early; this is part of the type check |
|
modns = mod.__dict__ |
|
# Parse it into package name and module name, e.g. 'foo.bar' and 'whatever' |
|
i = modname.rfind(".") |
|
if i >= 0: |
|
pkgname, modname = modname[:i], modname[i+1:] |
|
else: |
|
pkgname = None |
|
# Compute the search path |
|
if pkgname: |
|
# We're not reloading the package, only the module in it |
|
pkg = sys.modules[pkgname] |
|
if path is None: |
|
path = pkg.__path__ # Search inside the current parent package |
|
else: |
|
ps = pkgname.split('.') |
|
import os.path |
|
print path |
|
path = [os.path.join(*([p] + ps)) for p in path] |
|
else: |
|
# Search the top-level module path |
|
pkg = None |
|
# path = None # Make find_module() use the default search path |
|
print "xreload is using path=%r" % path |
|
# Find the module; may raise ImportError |
|
(stream, filename, (suffix, mode, kind)) = imp.find_module(modname, path) |
|
# Turn it into a code object |
|
try: |
|
# Is it Python source code or byte code read from a file? |
|
if kind not in (imp.PY_COMPILED, imp.PY_SOURCE): |
|
# Fall back to built-in reload() |
|
return reload(mod) |
|
if kind == imp.PY_SOURCE: |
|
source = stream.read() |
|
code = compile(source, filename, "exec") |
|
else: |
|
code = marshal.load(stream) |
|
finally: |
|
if stream: |
|
stream.close() |
|
# Execute the code. We copy the module dict to a temporary; then |
|
# clear the module dict; then execute the new code in the module |
|
# dict; then swap things back and around. This trick (due to |
|
# Glyph Lefkowitz) ensures that the (readonly) __globals__ |
|
# attribute of methods and functions is set to the correct dict |
|
# object. |
|
tmpns = modns.copy() |
|
modns.clear() |
|
modns["__name__"] = tmpns["__name__"] |
|
exec(code, modns) |
|
# Now we get to the hard part |
|
oldnames = set(tmpns) |
|
newnames = set(modns) |
|
# Update attributes in place |
|
for name in oldnames & newnames: |
|
modns[name] = _update(tmpns[name], modns[name]) |
|
# Don't lose our filename, for inspect |
|
modns["__file__"] = filename |
|
# Done! |
|
return mod |
|
|
|
|
|
def _update(oldobj, newobj): |
|
"""Update oldobj, if possible in place, with newobj. |
|
|
|
If oldobj is immutable, this simply returns newobj. |
|
|
|
Args: |
|
oldobj: the object to be updated |
|
newobj: the object used as the source for the update |
|
|
|
Returns: |
|
either oldobj, updated in place, or newobj. |
|
""" |
|
if oldobj is newobj: |
|
# Probably something imported |
|
return newobj |
|
if type(oldobj) is not type(newobj): |
|
# Cop-out: if the type changed, give up |
|
return newobj |
|
if hasattr(newobj, "__reload_update__"): |
|
# Provide a hook for updating |
|
return newobj.__reload_update__(oldobj) |
|
if isinstance(newobj, types.ClassType): |
|
return _update_class(oldobj, newobj) |
|
if isinstance(newobj, types.FunctionType): |
|
return _update_function(oldobj, newobj) |
|
if isinstance(newobj, types.MethodType): |
|
return _update_method(oldobj, newobj) |
|
if isinstance(newobj, classmethod): |
|
return _update_classmethod(oldobj, newobj) |
|
if isinstance(newobj, staticmethod): |
|
return _update_staticmethod(oldobj, newobj) |
|
# Not something we recognize, just give up |
|
return newobj |
|
|
|
|
|
# All of the following functions have the same signature as _update() |
|
|
|
|
|
def _update_function(oldfunc, newfunc): |
|
"""Update a function object.""" |
|
oldfunc.__doc__ = newfunc.__doc__ |
|
oldfunc.__dict__.update(newfunc.__dict__) |
|
oldfunc.func_code = newfunc.func_code |
|
oldfunc.func_defaults = newfunc.func_defaults |
|
return oldfunc |
|
|
|
|
|
def _update_method(oldmeth, newmeth): |
|
"""Update a method object.""" |
|
# XXX What if im_func is not a function? |
|
_update(oldmeth.im_func, newmeth.im_func) |
|
return oldmeth |
|
|
|
|
|
def _update_class(oldclass, newclass): |
|
"""Update a class object.""" |
|
olddict = oldclass.__dict__ |
|
newdict = newclass.__dict__ |
|
oldnames = set(olddict) |
|
newnames = set(newdict) |
|
for name in newnames - oldnames: |
|
setattr(oldclass, name, newdict[name]) |
|
for name in oldnames - newnames: |
|
delattr(oldclass, name) |
|
for name in oldnames & newnames - set(["__dict__", "__doc__"]): |
|
setattr(oldclass, name, _update(olddict[name], newdict[name])) |
|
return oldclass |
|
|
|
|
|
def _update_classmethod(oldcm, newcm): |
|
"""Update a classmethod update.""" |
|
# While we can't modify the classmethod object itself (it has no |
|
# mutable attributes), we *can* extract the underlying function |
|
# (by calling __get__(), which returns a method object) and update |
|
# it in-place. We don't have the class available to pass to |
|
# __get__() but any object except None will do. |
|
_update(oldcm.__get__(0), newcm.__get__(0)) |
|
return newcm |
|
|
|
|
|
def _update_staticmethod(oldsm, newsm): |
|
"""Update a staticmethod update.""" |
|
# While we can't modify the staticmethod object itself (it has no |
|
# mutable attributes), we *can* extract the underlying function |
|
# (by calling __get__(), which returns it) and update it in-place. |
|
# We don't have the class available to pass to __get__() but any |
|
# object except None will do. |
|
_update(oldsm.__get__(0), newsm.__get__(0)) |
|
return newsm
|
|
|