Attachment 'xreload.py'

Download

   1 """Alternative to reload().
   2 
   3 This works by executing the module in a scratch namespace, and then
   4 patching classes, methods and functions in place.  This avoids the
   5 need to patch instances.  New objects are copied into the target
   6 namespace.
   7 
   8 Some of the many limitiations include:
   9 
  10 - Global mutable objects other than classes are simply replaced, not patched
  11 
  12 - Code using metaclasses is not handled correctly
  13 
  14 - Code creating global singletons is not handled correctly
  15 
  16 - Functions and methods using decorators (other than classmethod and
  17   staticmethod) is not handled correctly
  18 
  19 - Renamings are not handled correctly
  20 
  21 - Dependent modules are not reloaded
  22 
  23 - When a dependent module contains 'from foo import bar', and
  24   reloading foo deletes foo.bar, the dependent module continues to use
  25   the old foo.bar object rather than failing
  26 
  27 - Frozen modules and modules loaded from zip files aren't handled
  28   correctly
  29 
  30 - Classes involving __slots__ are not handled correctly
  31 """
  32 
  33 import imp
  34 import sys
  35 import types
  36 
  37 
  38 def xreload(mod, path=None):
  39     """Reload a module in place, updating classes, methods and functions.
  40     
  41     mod can live in a package.  If path is None, we try to find mod from mod's
  42     packages's path.  If path is not None, we try to use it as the list of top
  43     level packages for mod.  For example:
  44 
  45     mod = xxx.yyy.m.  If path is None, we try to find m in xxx.yyy.__path__.
  46     If path is ['path1/', 'path2/'], we try to find m in 'path1/xxx/yyy' first
  47     and then in 'path2/xxx/yyy'.
  48 
  49     Args:
  50       mod: a module object
  51 
  52     Returns:
  53       The (updated) input object itself.
  54     """
  55     # Get the module name, e.g. 'foo.bar.whatever'
  56     modname = mod.__name__
  57     # Get the module namespace (dict) early; this is part of the type check
  58     modns = mod.__dict__
  59     # Parse it into package name and module name, e.g. 'foo.bar' and 'whatever'
  60     i = modname.rfind(".")
  61     if i >= 0:
  62         pkgname, modname = modname[:i], modname[i+1:]
  63     else:
  64         pkgname = None
  65     # Compute the search path
  66     if pkgname:
  67         # We're not reloading the package, only the module in it
  68         pkg = sys.modules[pkgname]
  69         if path is None:
  70             path = pkg.__path__  # Search inside the current parent package
  71         else:
  72             ps = pkgname.split('.')
  73             import os.path
  74             print path
  75             path = [os.path.join(*([p] + ps)) for p in path]
  76     else:
  77         # Search the top-level module path
  78         pkg  = None
  79         # path = None  # Make find_module() use the default search path
  80     print "xreload is using path=%r" % path
  81     # Find the module; may raise ImportError
  82     (stream, filename, (suffix, mode, kind)) = imp.find_module(modname, path)
  83     # Turn it into a code object
  84     try:
  85         # Is it Python source code or byte code read from a file?
  86         if kind not in (imp.PY_COMPILED, imp.PY_SOURCE):
  87             # Fall back to built-in reload()
  88             return reload(mod)
  89         if kind == imp.PY_SOURCE:
  90             source = stream.read()
  91             code = compile(source, filename, "exec")
  92         else:
  93             code = marshal.load(stream)
  94     finally:
  95         if stream:
  96             stream.close()
  97     # Execute the code.  We copy the module dict to a temporary; then
  98     # clear the module dict; then execute the new code in the module
  99     # dict; then swap things back and around.  This trick (due to
 100     # Glyph Lefkowitz) ensures that the (readonly) __globals__
 101     # attribute of methods and functions is set to the correct dict
 102     # object.
 103     tmpns = modns.copy()
 104     modns.clear()
 105     modns["__name__"] = tmpns["__name__"]
 106     exec(code, modns)
 107     # Now we get to the hard part
 108     oldnames = set(tmpns)
 109     newnames = set(modns)
 110     # Update attributes in place
 111     for name in oldnames & newnames:
 112         modns[name] = _update(tmpns[name], modns[name])
 113     # Don't lose our filename, for inspect
 114     modns["__file__"] = filename
 115     # Done!
 116     return mod
 117 
 118 
 119 def _update(oldobj, newobj):
 120     """Update oldobj, if possible in place, with newobj.
 121 
 122     If oldobj is immutable, this simply returns newobj.
 123 
 124     Args:
 125       oldobj: the object to be updated
 126       newobj: the object used as the source for the update
 127 
 128     Returns:
 129       either oldobj, updated in place, or newobj.
 130     """
 131     if oldobj is newobj:
 132         # Probably something imported
 133         return newobj
 134     if type(oldobj) is not type(newobj):
 135         # Cop-out: if the type changed, give up
 136         return newobj
 137     if hasattr(newobj, "__reload_update__"):
 138         # Provide a hook for updating
 139         return newobj.__reload_update__(oldobj)
 140     if isinstance(newobj, types.ClassType):
 141         return _update_class(oldobj, newobj)
 142     if isinstance(newobj, types.FunctionType):
 143         return _update_function(oldobj, newobj)
 144     if isinstance(newobj, types.MethodType):
 145         return _update_method(oldobj, newobj)
 146     if isinstance(newobj, classmethod):
 147         return _update_classmethod(oldobj, newobj)
 148     if isinstance(newobj, staticmethod):
 149         return _update_staticmethod(oldobj, newobj)
 150     # Not something we recognize, just give up
 151     return newobj
 152 
 153 
 154 # All of the following functions have the same signature as _update()
 155 
 156 
 157 def _update_function(oldfunc, newfunc):
 158     """Update a function object."""
 159     oldfunc.__doc__ = newfunc.__doc__
 160     oldfunc.__dict__.update(newfunc.__dict__)
 161     oldfunc.func_code = newfunc.func_code
 162     oldfunc.func_defaults = newfunc.func_defaults
 163     return oldfunc
 164 
 165 
 166 def _update_method(oldmeth, newmeth):
 167     """Update a method object."""
 168     # XXX What if im_func is not a function?
 169     _update(oldmeth.im_func, newmeth.im_func)
 170     return oldmeth
 171 
 172 
 173 def _update_class(oldclass, newclass):
 174     """Update a class object."""
 175     olddict = oldclass.__dict__
 176     newdict = newclass.__dict__
 177     oldnames = set(olddict)
 178     newnames = set(newdict)
 179     for name in newnames - oldnames:
 180         setattr(oldclass, name, newdict[name])
 181     for name in oldnames - newnames:
 182         delattr(oldclass, name)
 183     for name in oldnames & newnames - set(["__dict__", "__doc__"]):
 184         setattr(oldclass, name,  _update(olddict[name], newdict[name]))
 185     return oldclass
 186 
 187 
 188 def _update_classmethod(oldcm, newcm):
 189     """Update a classmethod update."""
 190     # While we can't modify the classmethod object itself (it has no
 191     # mutable attributes), we *can* extract the underlying function
 192     # (by calling __get__(), which returns a method object) and update
 193     # it in-place.  We don't have the class available to pass to
 194     # __get__() but any object except None will do.
 195     _update(oldcm.__get__(0), newcm.__get__(0))
 196     return newcm
 197 
 198 
 199 def _update_staticmethod(oldsm, newsm):
 200     """Update a staticmethod update."""
 201     # While we can't modify the staticmethod object itself (it has no
 202     # mutable attributes), we *can* extract the underlying function
 203     # (by calling __get__(), which returns it) and update it in-place.
 204     # We don't have the class available to pass to __get__() but any
 205     # object except None will do.
 206     _update(oldsm.__get__(0), newsm.__get__(0))
 207     return newsm

Attached Files

To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.

You are not allowed to attach a file to this page.