| # -*- test-case-name: twisted.test.test_persisted -*- |
| |
| # Copyright (c) 2001-2004 Twisted Matrix Laboratories. |
| # See LICENSE for details. |
| |
| |
| |
| """ |
| AOT: Abstract Object Trees |
| The source-code-marshallin'est abstract-object-serializin'est persister |
| this side of Marmalade! |
| """ |
| |
| import types, new, string, copy_reg, tokenize, re |
| |
| from twisted.python import reflect, log |
| from twisted.persisted import crefutil |
| |
| ########################### |
| # Abstract Object Classes # |
| ########################### |
| |
| #"\0" in a getSource means "insert variable-width indention here". |
| #see `indentify'. |
| |
| class Named: |
| def __init__(self, name): |
| self.name = name |
| |
| class Class(Named): |
| def getSource(self): |
| return "Class(%r)" % self.name |
| |
| class Function(Named): |
| def getSource(self): |
| return "Function(%r)" % self.name |
| |
| class Module(Named): |
| def getSource(self): |
| return "Module(%r)" % self.name |
| |
| |
| class InstanceMethod: |
| def __init__(self, name, klass, inst): |
| if not (isinstance(inst, Ref) or isinstance(inst, Instance) or isinstance(inst, Deref)): |
| raise TypeError("%s isn't an Instance, Ref, or Deref!" % inst) |
| self.name = name |
| self.klass = klass |
| self.instance = inst |
| |
| def getSource(self): |
| return "InstanceMethod(%r, %r, \n\0%s)" % (self.name, self.klass, prettify(self.instance)) |
| |
| |
| class _NoStateObj: |
| pass |
| NoStateObj = _NoStateObj() |
| |
| _SIMPLE_BUILTINS = [ |
| types.StringType, types.UnicodeType, types.IntType, types.FloatType, |
| types.ComplexType, types.LongType, types.NoneType, types.SliceType, |
| types.EllipsisType] |
| |
| try: |
| _SIMPLE_BUILTINS.append(types.BooleanType) |
| except AttributeError: |
| pass |
| |
| class Instance: |
| def __init__(self, className, __stateObj__=NoStateObj, **state): |
| if not isinstance(className, types.StringType): |
| raise TypeError("%s isn't a string!" % className) |
| self.klass = className |
| if __stateObj__ is not NoStateObj: |
| self.state = __stateObj__ |
| self.stateIsDict = 0 |
| else: |
| self.state = state |
| self.stateIsDict = 1 |
| |
| def getSource(self): |
| #XXX make state be foo=bar instead of a dict. |
| if self.stateIsDict: |
| stateDict = self.state |
| elif isinstance(self.state, Ref) and isinstance(self.state.obj, types.DictType): |
| stateDict = self.state.obj |
| else: |
| stateDict = None |
| if stateDict is not None: |
| try: |
| return "Instance(%r, %s)" % (self.klass, dictToKW(stateDict)) |
| except NonFormattableDict: |
| return "Instance(%r, %s)" % (self.klass, prettify(stateDict)) |
| return "Instance(%r, %s)" % (self.klass, prettify(self.state)) |
| |
| class Ref: |
| |
| def __init__(self, *args): |
| #blargh, lame. |
| if len(args) == 2: |
| self.refnum = args[0] |
| self.obj = args[1] |
| elif not args: |
| self.refnum = None |
| self.obj = None |
| |
| def setRef(self, num): |
| if self.refnum: |
| raise ValueError("Error setting id %s, I already have %s" % (num, self.refnum)) |
| self.refnum = num |
| |
| def setObj(self, obj): |
| if self.obj: |
| raise ValueError("Error setting obj %s, I already have %s" % (obj, self.obj)) |
| self.obj = obj |
| |
| def getSource(self): |
| if self.obj is None: |
| raise RuntimeError("Don't try to display me before setting an object on me!") |
| if self.refnum: |
| return "Ref(%d, \n\0%s)" % (self.refnum, prettify(self.obj)) |
| return prettify(self.obj) |
| |
| |
| class Deref: |
| def __init__(self, num): |
| self.refnum = num |
| |
| def getSource(self): |
| return "Deref(%d)" % self.refnum |
| |
| __repr__ = getSource |
| |
| |
| class Copyreg: |
| def __init__(self, loadfunc, state): |
| self.loadfunc = loadfunc |
| self.state = state |
| |
| def getSource(self): |
| return "Copyreg(%r, %s)" % (self.loadfunc, prettify(self.state)) |
| |
| |
| |
| ############### |
| # Marshalling # |
| ############### |
| |
| |
| def getSource(ao): |
| """Pass me an AO, I'll return a nicely-formatted source representation.""" |
| return indentify("app = " + prettify(ao)) |
| |
| |
| class NonFormattableDict(Exception): |
| """A dictionary was not formattable. |
| """ |
| |
| r = re.compile('[a-zA-Z_][a-zA-Z0-9_]*$') |
| |
| def dictToKW(d): |
| out = [] |
| items = d.items() |
| items.sort() |
| for k,v in items: |
| if not isinstance(k, types.StringType): |
| raise NonFormattableDict("%r ain't a string" % k) |
| if not r.match(k): |
| raise NonFormattableDict("%r ain't an identifier" % k) |
| out.append( |
| "\n\0%s=%s," % (k, prettify(v)) |
| ) |
| return string.join(out, '') |
| |
| |
| def prettify(obj): |
| if hasattr(obj, 'getSource'): |
| return obj.getSource() |
| else: |
| #basic type |
| t = type(obj) |
| |
| if t in _SIMPLE_BUILTINS: |
| return repr(obj) |
| |
| elif t is types.DictType: |
| out = ['{'] |
| for k,v in obj.items(): |
| out.append('\n\0%s: %s,' % (prettify(k), prettify(v))) |
| out.append(len(obj) and '\n\0}' or '}') |
| return string.join(out, '') |
| |
| elif t is types.ListType: |
| out = ["["] |
| for x in obj: |
| out.append('\n\0%s,' % prettify(x)) |
| out.append(len(obj) and '\n\0]' or ']') |
| return string.join(out, '') |
| |
| elif t is types.TupleType: |
| out = ["("] |
| for x in obj: |
| out.append('\n\0%s,' % prettify(x)) |
| out.append(len(obj) and '\n\0)' or ')') |
| return string.join(out, '') |
| else: |
| raise TypeError("Unsupported type %s when trying to prettify %s." % (t, obj)) |
| |
| def indentify(s): |
| out = [] |
| stack = [] |
| def eater(type, val, r, c, l, out=out, stack=stack): |
| #import sys |
| #sys.stdout.write(val) |
| if val in ['[', '(', '{']: |
| stack.append(val) |
| elif val in [']', ')', '}']: |
| stack.pop() |
| if val == '\0': |
| out.append(' '*len(stack)) |
| else: |
| out.append(val) |
| l = ['', s] |
| tokenize.tokenize(l.pop, eater) |
| return string.join(out, '') |
| |
| |
| |
| |
| |
| ########### |
| # Unjelly # |
| ########### |
| |
| def unjellyFromAOT(aot): |
| """ |
| Pass me an Abstract Object Tree, and I'll unjelly it for you. |
| """ |
| return AOTUnjellier().unjelly(aot) |
| |
| def unjellyFromSource(stringOrFile): |
| """ |
| Pass me a string of code or a filename that defines an 'app' variable (in |
| terms of Abstract Objects!), and I'll execute it and unjelly the resulting |
| AOT for you, returning a newly unpersisted Application object! |
| """ |
| |
| ns = {"Instance": Instance, |
| "InstanceMethod": InstanceMethod, |
| "Class": Class, |
| "Function": Function, |
| "Module": Module, |
| "Ref": Ref, |
| "Deref": Deref, |
| "Copyreg": Copyreg, |
| } |
| |
| if hasattr(stringOrFile, "read"): |
| exec stringOrFile.read() in ns |
| else: |
| exec stringOrFile in ns |
| |
| if ns.has_key('app'): |
| return unjellyFromAOT(ns['app']) |
| else: |
| raise ValueError("%s needs to define an 'app', it didn't!" % stringOrFile) |
| |
| |
| class AOTUnjellier: |
| """I handle the unjellying of an Abstract Object Tree. |
| See AOTUnjellier.unjellyAO |
| """ |
| def __init__(self): |
| self.references = {} |
| self.stack = [] |
| self.afterUnjelly = [] |
| |
| ## |
| # unjelly helpers (copied pretty much directly from (now deleted) marmalade) |
| ## |
| def unjellyLater(self, node): |
| """Unjelly a node, later. |
| """ |
| d = crefutil._Defer() |
| self.unjellyInto(d, 0, node) |
| return d |
| |
| def unjellyInto(self, obj, loc, ao): |
| """Utility method for unjellying one object into another. |
| This automates the handling of backreferences. |
| """ |
| o = self.unjellyAO(ao) |
| obj[loc] = o |
| if isinstance(o, crefutil.NotKnown): |
| o.addDependant(obj, loc) |
| return o |
| |
| def callAfter(self, callable, result): |
| if isinstance(result, crefutil.NotKnown): |
| l = [None] |
| result.addDependant(l, 1) |
| else: |
| l = [result] |
| self.afterUnjelly.append((callable, l)) |
| |
| def unjellyAttribute(self, instance, attrName, ao): |
| #XXX this is unused???? |
| """Utility method for unjellying into instances of attributes. |
| |
| Use this rather than unjellyAO unless you like surprising bugs! |
| Alternatively, you can use unjellyInto on your instance's __dict__. |
| """ |
| self.unjellyInto(instance.__dict__, attrName, ao) |
| |
| def unjellyAO(self, ao): |
| """Unjelly an Abstract Object and everything it contains. |
| I return the real object. |
| """ |
| self.stack.append(ao) |
| t = type(ao) |
| if t is types.InstanceType: |
| #Abstract Objects |
| c = ao.__class__ |
| if c is Module: |
| return reflect.namedModule(ao.name) |
| |
| elif c in [Class, Function] or issubclass(c, type): |
| return reflect.namedObject(ao.name) |
| |
| elif c is InstanceMethod: |
| im_name = ao.name |
| im_class = reflect.namedObject(ao.klass) |
| im_self = self.unjellyAO(ao.instance) |
| if im_name in im_class.__dict__: |
| if im_self is None: |
| return getattr(im_class, im_name) |
| elif isinstance(im_self, crefutil.NotKnown): |
| return crefutil._InstanceMethod(im_name, im_self, im_class) |
| else: |
| return new.instancemethod(im_class.__dict__[im_name], |
| im_self, |
| im_class) |
| else: |
| raise TypeError("instance method changed") |
| |
| elif c is Instance: |
| klass = reflect.namedObject(ao.klass) |
| state = self.unjellyAO(ao.state) |
| if hasattr(klass, "__setstate__"): |
| inst = new.instance(klass, {}) |
| self.callAfter(inst.__setstate__, state) |
| else: |
| inst = new.instance(klass, state) |
| return inst |
| |
| elif c is Ref: |
| o = self.unjellyAO(ao.obj) #THIS IS CHANGING THE REF OMG |
| refkey = ao.refnum |
| ref = self.references.get(refkey) |
| if ref is None: |
| self.references[refkey] = o |
| elif isinstance(ref, crefutil.NotKnown): |
| ref.resolveDependants(o) |
| self.references[refkey] = o |
| elif refkey is None: |
| # This happens when you're unjellying from an AOT not read from source |
| pass |
| else: |
| raise ValueError("Multiple references with the same ID: %s, %s, %s!" % (ref, refkey, ao)) |
| return o |
| |
| elif c is Deref: |
| num = ao.refnum |
| ref = self.references.get(num) |
| if ref is None: |
| der = crefutil._Dereference(num) |
| self.references[num] = der |
| return der |
| return ref |
| |
| elif c is Copyreg: |
| loadfunc = reflect.namedObject(ao.loadfunc) |
| d = self.unjellyLater(ao.state).addCallback( |
| lambda result, _l: apply(_l, result), loadfunc) |
| return d |
| |
| #Types |
| |
| elif t in _SIMPLE_BUILTINS: |
| return ao |
| |
| elif t is types.ListType: |
| l = [] |
| for x in ao: |
| l.append(None) |
| self.unjellyInto(l, len(l)-1, x) |
| return l |
| |
| elif t is types.TupleType: |
| l = [] |
| tuple_ = tuple |
| for x in ao: |
| l.append(None) |
| if isinstance(self.unjellyInto(l, len(l)-1, x), crefutil.NotKnown): |
| tuple_ = crefutil._Tuple |
| return tuple_(l) |
| |
| elif t is types.DictType: |
| d = {} |
| for k,v in ao.items(): |
| kvd = crefutil._DictKeyAndValue(d) |
| self.unjellyInto(kvd, 0, k) |
| self.unjellyInto(kvd, 1, v) |
| return d |
| |
| else: |
| raise TypeError("Unsupported AOT type: %s" % t) |
| |
| del self.stack[-1] |
| |
| |
| def unjelly(self, ao): |
| try: |
| l = [None] |
| self.unjellyInto(l, 0, ao) |
| for callable, v in self.afterUnjelly: |
| callable(v[0]) |
| return l[0] |
| except: |
| log.msg("Error jellying object! Stacktrace follows::") |
| log.msg(string.join(map(repr, self.stack), "\n")) |
| raise |
| ######### |
| # Jelly # |
| ######### |
| |
| |
| def jellyToAOT(obj): |
| """Convert an object to an Abstract Object Tree.""" |
| return AOTJellier().jelly(obj) |
| |
| def jellyToSource(obj, file=None): |
| """ |
| Pass me an object and, optionally, a file object. |
| I'll convert the object to an AOT either return it (if no file was |
| specified) or write it to the file. |
| """ |
| |
| aot = jellyToAOT(obj) |
| if file: |
| file.write(getSource(aot)) |
| else: |
| return getSource(aot) |
| |
| |
| class AOTJellier: |
| def __init__(self): |
| # dict of {id(obj): (obj, node)} |
| self.prepared = {} |
| self._ref_id = 0 |
| self.stack = [] |
| |
| def prepareForRef(self, aoref, object): |
| """I prepare an object for later referencing, by storing its id() |
| and its _AORef in a cache.""" |
| self.prepared[id(object)] = aoref |
| |
| def jellyToAO(self, obj): |
| """I turn an object into an AOT and return it.""" |
| objType = type(obj) |
| self.stack.append(repr(obj)) |
| |
| #immutable: We don't care if these have multiple refs! |
| if objType in _SIMPLE_BUILTINS: |
| retval = obj |
| |
| elif objType is types.MethodType: |
| # TODO: make methods 'prefer' not to jelly the object internally, |
| # so that the object will show up where it's referenced first NOT |
| # by a method. |
| retval = InstanceMethod(obj.im_func.__name__, reflect.qual(obj.im_class), |
| self.jellyToAO(obj.im_self)) |
| |
| elif objType is types.ModuleType: |
| retval = Module(obj.__name__) |
| |
| elif objType is types.ClassType: |
| retval = Class(reflect.qual(obj)) |
| |
| elif issubclass(objType, type): |
| retval = Class(reflect.qual(obj)) |
| |
| elif objType is types.FunctionType: |
| retval = Function(reflect.fullFuncName(obj)) |
| |
| else: #mutable! gotta watch for refs. |
| |
| #Marmalade had the nicety of being able to just stick a 'reference' attribute |
| #on any Node object that was referenced, but in AOT, the referenced object |
| #is *inside* of a Ref call (Ref(num, obj) instead of |
| #<objtype ... reference="1">). The problem is, especially for built-in types, |
| #I can't just assign some attribute to them to give them a refnum. So, I have |
| #to "wrap" a Ref(..) around them later -- that's why I put *everything* that's |
| #mutable inside one. The Ref() class will only print the "Ref(..)" around an |
| #object if it has a Reference explicitly attached. |
| |
| if self.prepared.has_key(id(obj)): |
| oldRef = self.prepared[id(obj)] |
| if oldRef.refnum: |
| # it's been referenced already |
| key = oldRef.refnum |
| else: |
| # it hasn't been referenced yet |
| self._ref_id = self._ref_id + 1 |
| key = self._ref_id |
| oldRef.setRef(key) |
| return Deref(key) |
| |
| retval = Ref() |
| self.prepareForRef(retval, obj) |
| |
| if objType is types.ListType: |
| retval.setObj(map(self.jellyToAO, obj)) #hah! |
| |
| elif objType is types.TupleType: |
| retval.setObj(tuple(map(self.jellyToAO, obj))) |
| |
| elif objType is types.DictionaryType: |
| d = {} |
| for k,v in obj.items(): |
| d[self.jellyToAO(k)] = self.jellyToAO(v) |
| retval.setObj(d) |
| |
| elif objType is types.InstanceType: |
| if hasattr(obj, "__getstate__"): |
| state = self.jellyToAO(obj.__getstate__()) |
| else: |
| state = self.jellyToAO(obj.__dict__) |
| retval.setObj(Instance(reflect.qual(obj.__class__), state)) |
| |
| elif copy_reg.dispatch_table.has_key(objType): |
| unpickleFunc, state = copy_reg.dispatch_table[objType](obj) |
| |
| retval.setObj(Copyreg( reflect.fullFuncName(unpickleFunc), |
| self.jellyToAO(state))) |
| |
| else: |
| raise TypeError("Unsupported type: %s" % objType.__name__) |
| |
| del self.stack[-1] |
| return retval |
| |
| def jelly(self, obj): |
| try: |
| ao = self.jellyToAO(obj) |
| return ao |
| except: |
| log.msg("Error jellying object! Stacktrace follows::") |
| log.msg(string.join(self.stack, '\n')) |
| raise |