Moving xreload to the sandbox for now.
This commit is contained in:
parent
e27dc72308
commit
cef2098cfa
|
@ -1,103 +0,0 @@
|
|||
"""Doctests for module reloading.
|
||||
|
||||
>>> from xreload import xreload
|
||||
>>> from test.test_xreload import make_mod
|
||||
>>> make_mod()
|
||||
>>> import x
|
||||
>>> C = x.C
|
||||
>>> Cfoo = C.foo
|
||||
>>> Cbar = C.bar
|
||||
>>> Cstomp = C.stomp
|
||||
>>> b = C()
|
||||
>>> bfoo = b.foo
|
||||
>>> b.foo()
|
||||
42
|
||||
>>> bfoo()
|
||||
42
|
||||
>>> Cfoo(b)
|
||||
42
|
||||
>>> Cbar()
|
||||
42 42
|
||||
>>> Cstomp()
|
||||
42 42 42
|
||||
>>> make_mod(repl="42", subst="24")
|
||||
>>> xreload(x)
|
||||
<module 'x' (built-in)>
|
||||
>>> b.foo()
|
||||
24
|
||||
>>> bfoo()
|
||||
24
|
||||
>>> Cfoo(b)
|
||||
24
|
||||
>>> Cbar()
|
||||
24 24
|
||||
>>> Cstomp()
|
||||
24 24 24
|
||||
|
||||
"""
|
||||
|
||||
SAMPLE_CODE = """
|
||||
class C:
|
||||
def foo(self):
|
||||
print(42)
|
||||
@classmethod
|
||||
def bar(cls):
|
||||
print(42, 42)
|
||||
@staticmethod
|
||||
def stomp():
|
||||
print (42, 42, 42)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import doctest
|
||||
import xreload
|
||||
import tempfile
|
||||
from test.test_support import run_unittest
|
||||
|
||||
tempdir = None
|
||||
save_path = None
|
||||
|
||||
|
||||
def setUp(unused=None):
|
||||
global tempdir, save_path
|
||||
tempdir = tempfile.mkdtemp()
|
||||
save_path = list(sys.path)
|
||||
sys.path.append(tempdir)
|
||||
|
||||
|
||||
def tearDown(unused=None):
|
||||
global tempdir, save_path
|
||||
if save_path is not None:
|
||||
sys.path = save_path
|
||||
save_path = None
|
||||
if tempdir is not None:
|
||||
shutil.rmtree(tempdir)
|
||||
tempdir = None
|
||||
|
||||
|
||||
def make_mod(name="x", repl=None, subst=None):
|
||||
if not tempdir:
|
||||
setUp()
|
||||
assert tempdir
|
||||
fn = os.path.join(tempdir, name + ".py")
|
||||
f = open(fn, "w")
|
||||
sample = SAMPLE_CODE
|
||||
if repl is not None and subst is not None:
|
||||
sample = sample.replace(repl, subst)
|
||||
try:
|
||||
f.write(sample)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
|
||||
def test_suite():
|
||||
return doctest.DocTestSuite(setUp=setUp, tearDown=tearDown)
|
||||
|
||||
|
||||
def test_main():
|
||||
run_unittest(test_suite())
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_main()
|
190
Lib/xreload.py
190
Lib/xreload.py
|
@ -1,190 +0,0 @@
|
|||
"""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):
|
||||
"""Reload a module in place, updating classes, methods and functions.
|
||||
|
||||
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]
|
||||
path = pkg.__path__ # Search inside the package
|
||||
else:
|
||||
# Search the top-level module path
|
||||
pkg = None
|
||||
path = None # Make find_module() uses the default search 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])
|
||||
# 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.__code__ = newfunc.__code__
|
||||
oldfunc.__defaults__ = newfunc.__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 - {"__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
|
Loading…
Reference in New Issue