Viewing file: rebuild.py (6.96 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
# -*- 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 imp 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
|