251 lines
7.0 KiB
Python
251 lines
7.0 KiB
Python
![]() |
# -*- test-case-name: twisted.test.test_rebuild -*-
|
||
|
# Copyright (c) Twisted Matrix Laboratories.
|
||
|
# See LICENSE for details.
|
||
|
|
||
|
|
||
|
"""
|
||
|
*Real* reloading support for Python.
|
||
|
"""
|
||
|
|
||
|
import linecache
|
||
|
|
||
|
# System Imports
|
||
|
import sys
|
||
|
import time
|
||
|
import types
|
||
|
from importlib import reload
|
||
|
from types import ModuleType
|
||
|
from typing import Dict
|
||
|
|
||
|
# Sibling Imports
|
||
|
from twisted.python import log, reflect
|
||
|
|
||
|
lastRebuild = time.time()
|
||
|
|
||
|
|
||
|
class Sensitive:
|
||
|
"""
|
||
|
A utility mixin that's sensitive to rebuilds.
|
||
|
|
||
|
This is a mixin for classes (usually those which represent collections of
|
||
|
callbacks) to make sure that their code is up-to-date before running.
|
||
|
"""
|
||
|
|
||
|
lastRebuild = lastRebuild
|
||
|
|
||
|
def needRebuildUpdate(self):
|
||
|
yn = self.lastRebuild < lastRebuild
|
||
|
return yn
|
||
|
|
||
|
def rebuildUpToDate(self):
|
||
|
self.lastRebuild = time.time()
|
||
|
|
||
|
def latestVersionOf(self, anObject):
|
||
|
"""
|
||
|
Get the latest version of an object.
|
||
|
|
||
|
This can handle just about anything callable; instances, functions,
|
||
|
methods, and classes.
|
||
|
"""
|
||
|
t = type(anObject)
|
||
|
if t == types.FunctionType:
|
||
|
return latestFunction(anObject)
|
||
|
elif t == types.MethodType:
|
||
|
if anObject.__self__ is None:
|
||
|
return getattr(anObject.im_class, anObject.__name__)
|
||
|
else:
|
||
|
return getattr(anObject.__self__, anObject.__name__)
|
||
|
else:
|
||
|
log.msg("warning returning anObject!")
|
||
|
return anObject
|
||
|
|
||
|
|
||
|
_modDictIDMap: Dict[int, ModuleType] = {}
|
||
|
|
||
|
|
||
|
def latestFunction(oldFunc):
|
||
|
"""
|
||
|
Get the latest version of a function.
|
||
|
"""
|
||
|
# This may be CPython specific, since I believe jython instantiates a new
|
||
|
# module upon reload.
|
||
|
dictID = id(oldFunc.__globals__)
|
||
|
module = _modDictIDMap.get(dictID)
|
||
|
if module is None:
|
||
|
return oldFunc
|
||
|
return getattr(module, oldFunc.__name__)
|
||
|
|
||
|
|
||
|
def latestClass(oldClass):
|
||
|
"""
|
||
|
Get the latest version of a class.
|
||
|
"""
|
||
|
module = reflect.namedModule(oldClass.__module__)
|
||
|
newClass = getattr(module, oldClass.__name__)
|
||
|
newBases = [latestClass(base) for base in newClass.__bases__]
|
||
|
|
||
|
if newClass.__module__ == "builtins":
|
||
|
# builtin members can't be reloaded sanely
|
||
|
return newClass
|
||
|
|
||
|
try:
|
||
|
# This makes old-style stuff work
|
||
|
newClass.__bases__ = tuple(newBases)
|
||
|
return newClass
|
||
|
except TypeError:
|
||
|
ctor = type(newClass)
|
||
|
return ctor(newClass.__name__, tuple(newBases), dict(newClass.__dict__))
|
||
|
|
||
|
|
||
|
class RebuildError(Exception):
|
||
|
"""
|
||
|
Exception raised when trying to rebuild a class whereas it's not possible.
|
||
|
"""
|
||
|
|
||
|
|
||
|
def updateInstance(self):
|
||
|
"""
|
||
|
Updates an instance to be current.
|
||
|
"""
|
||
|
self.__class__ = latestClass(self.__class__)
|
||
|
|
||
|
|
||
|
def __injectedgetattr__(self, name):
|
||
|
"""
|
||
|
A getattr method to cause a class to be refreshed.
|
||
|
"""
|
||
|
if name == "__del__":
|
||
|
raise AttributeError("Without this, Python segfaults.")
|
||
|
updateInstance(self)
|
||
|
log.msg(f"(rebuilding stale {reflect.qual(self.__class__)} instance ({name}))")
|
||
|
result = getattr(self, name)
|
||
|
return result
|
||
|
|
||
|
|
||
|
def rebuild(module, doLog=1):
|
||
|
"""
|
||
|
Reload a module and do as much as possible to replace its references.
|
||
|
"""
|
||
|
global lastRebuild
|
||
|
lastRebuild = time.time()
|
||
|
if hasattr(module, "ALLOW_TWISTED_REBUILD"):
|
||
|
# Is this module allowed to be rebuilt?
|
||
|
if not module.ALLOW_TWISTED_REBUILD:
|
||
|
raise RuntimeError("I am not allowed to be rebuilt.")
|
||
|
if doLog:
|
||
|
log.msg(f"Rebuilding {str(module.__name__)}...")
|
||
|
|
||
|
# Safely handle adapter re-registration
|
||
|
from twisted.python import components
|
||
|
|
||
|
components.ALLOW_DUPLICATES = True
|
||
|
|
||
|
d = module.__dict__
|
||
|
_modDictIDMap[id(d)] = module
|
||
|
newclasses = {}
|
||
|
classes = {}
|
||
|
functions = {}
|
||
|
values = {}
|
||
|
if doLog:
|
||
|
log.msg(f" (scanning {str(module.__name__)}): ")
|
||
|
for k, v in d.items():
|
||
|
if issubclass(type(v), types.FunctionType):
|
||
|
if v.__globals__ is module.__dict__:
|
||
|
functions[v] = 1
|
||
|
if doLog:
|
||
|
log.logfile.write("f")
|
||
|
log.logfile.flush()
|
||
|
elif isinstance(v, type):
|
||
|
if v.__module__ == module.__name__:
|
||
|
newclasses[v] = 1
|
||
|
if doLog:
|
||
|
log.logfile.write("o")
|
||
|
log.logfile.flush()
|
||
|
|
||
|
values.update(classes)
|
||
|
values.update(functions)
|
||
|
fromOldModule = values.__contains__
|
||
|
newclasses = newclasses.keys()
|
||
|
classes = classes.keys()
|
||
|
functions = functions.keys()
|
||
|
|
||
|
if doLog:
|
||
|
log.msg("")
|
||
|
log.msg(f" (reload {str(module.__name__)})")
|
||
|
|
||
|
# Boom.
|
||
|
reload(module)
|
||
|
# Make sure that my traceback printing will at least be recent...
|
||
|
linecache.clearcache()
|
||
|
|
||
|
if doLog:
|
||
|
log.msg(f" (cleaning {str(module.__name__)}): ")
|
||
|
|
||
|
for clazz in classes:
|
||
|
if getattr(module, clazz.__name__) is clazz:
|
||
|
log.msg(f"WARNING: class {reflect.qual(clazz)} not replaced by reload!")
|
||
|
else:
|
||
|
if doLog:
|
||
|
log.logfile.write("x")
|
||
|
log.logfile.flush()
|
||
|
clazz.__bases__ = ()
|
||
|
clazz.__dict__.clear()
|
||
|
clazz.__getattr__ = __injectedgetattr__
|
||
|
clazz.__module__ = module.__name__
|
||
|
if newclasses:
|
||
|
import gc
|
||
|
for nclass in newclasses:
|
||
|
ga = getattr(module, nclass.__name__)
|
||
|
if ga is nclass:
|
||
|
log.msg(
|
||
|
"WARNING: new-class {} not replaced by reload!".format(
|
||
|
reflect.qual(nclass)
|
||
|
)
|
||
|
)
|
||
|
else:
|
||
|
for r in gc.get_referrers(nclass):
|
||
|
if getattr(r, "__class__", None) is nclass:
|
||
|
r.__class__ = ga
|
||
|
if doLog:
|
||
|
log.msg("")
|
||
|
log.msg(f" (fixing {str(module.__name__)}): ")
|
||
|
modcount = 0
|
||
|
for mk, mod in sys.modules.items():
|
||
|
modcount = modcount + 1
|
||
|
if mod == module or mod is None:
|
||
|
continue
|
||
|
|
||
|
if not hasattr(mod, "__file__"):
|
||
|
# It's a builtin module; nothing to replace here.
|
||
|
continue
|
||
|
|
||
|
if hasattr(mod, "__bundle__"):
|
||
|
# PyObjC has a few buggy objects which segfault if you hash() them.
|
||
|
# It doesn't make sense to try rebuilding extension modules like
|
||
|
# this anyway, so don't try.
|
||
|
continue
|
||
|
|
||
|
changed = 0
|
||
|
|
||
|
for k, v in mod.__dict__.items():
|
||
|
try:
|
||
|
hash(v)
|
||
|
except Exception:
|
||
|
continue
|
||
|
if fromOldModule(v):
|
||
|
if doLog:
|
||
|
log.logfile.write("f")
|
||
|
log.logfile.flush()
|
||
|
nv = latestFunction(v)
|
||
|
changed = 1
|
||
|
setattr(mod, k, nv)
|
||
|
if doLog and not changed and ((modcount % 10) == 0):
|
||
|
log.logfile.write(".")
|
||
|
log.logfile.flush()
|
||
|
|
||
|
components.ALLOW_DUPLICATES = False
|
||
|
if doLog:
|
||
|
log.msg("")
|
||
|
log.msg(f" Rebuilt {str(module.__name__)}.")
|
||
|
return module
|