123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627 |
- # -*- test-case-name: twisted.test.test_persisted -*-
-
- # Copyright (c) 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 copyreg as copy_reg
- import re
- import types
- from tokenize import generate_tokens as tokenize
-
- from twisted.persisted import crefutil
- from twisted.python import log, reflect
- from twisted.python.compat import _constructMethod
-
- ###########################
- # 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{})".format(
- self.name,
- self.klass,
- prettify(self.instance),
- )
-
-
- class _NoStateObj:
- pass
-
-
- NoStateObj = _NoStateObj()
-
- _SIMPLE_BUILTINS = [
- bool,
- bytes,
- str,
- int,
- float,
- complex,
- type(None),
- slice,
- type(Ellipsis),
- ]
-
-
- class Instance:
- def __init__(self, className, __stateObj__=NoStateObj, **state):
- if not isinstance(className, str):
- 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, dict):
- stateDict = self.state.obj
- else:
- stateDict = None
- if stateDict is not None:
- try:
- return f"Instance({self.klass!r}, {dictToKW(stateDict)})"
- except NonFormattableDict:
- return f"Instance({self.klass!r}, {prettify(stateDict)})"
- return f"Instance({self.klass!r}, {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(f"Error setting id {num}, I already have {self.refnum}")
- self.refnum = num
-
- def setObj(self, obj):
- if self.obj:
- raise ValueError(f"Error setting obj {obj}, I already have {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 f"Copyreg({self.loadfunc!r}, {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 = list(d.items())
- items.sort()
- for k, v in items:
- if not isinstance(k, str):
- raise NonFormattableDict("%r ain't a string" % k)
- if not r.match(k):
- raise NonFormattableDict("%r ain't an identifier" % k)
- out.append(f"\n\0{k}={prettify(v)},")
- return "".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 dict:
- out = ["{"]
- for k, v in obj.items():
- out.append(f"\n\0{prettify(k)}: {prettify(v)},")
- out.append(len(obj) and "\n\0}" or "}")
- return "".join(out)
-
- elif t is list:
- out = ["["]
- for x in obj:
- out.append("\n\0%s," % prettify(x))
- out.append(len(obj) and "\n\0]" or "]")
- return "".join(out)
-
- elif t is tuple:
- out = ["("]
- for x in obj:
- out.append("\n\0%s," % prettify(x))
- out.append(len(obj) and "\n\0)" or ")")
- return "".join(out)
- else:
- raise TypeError(f"Unsupported type {t} when trying to prettify {obj}.")
-
-
- def indentify(s):
- out = []
- stack = []
- l = ["", s]
- for (
- tokenType,
- tokenString,
- (startRow, startColumn),
- (endRow, endColumn),
- logicalLine,
- ) in tokenize(l.pop):
- if tokenString in ["[", "(", "{"]:
- stack.append(tokenString)
- elif tokenString in ["]", ")", "}"]:
- stack.pop()
- if tokenString == "\0":
- out.append(" " * len(stack))
- else:
- out.append(tokenString)
- return "".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"):
- source = stringOrFile.read()
- else:
- source = stringOrFile
- code = compile(source, "<source>", "exec")
- eval(code, ns, ns)
-
- if "app" in ns:
- 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):
- listResult = [None]
- result.addDependant(listResult, 1)
- else:
- listResult = [result]
- self.afterUnjelly.append((callable, listResult))
-
- 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 in _SIMPLE_BUILTINS:
- return ao
-
- elif t is list:
- l = []
- for x in ao:
- l.append(None)
- self.unjellyInto(l, len(l) - 1, x)
- return l
-
- elif t is tuple:
- 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 dict:
- d = {}
- for k, v in ao.items():
- kvd = crefutil._DictKeyAndValue(d)
- self.unjellyInto(kvd, 0, k)
- self.unjellyInto(kvd, 1, v)
- return d
- else:
- # 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 _constructMethod(im_class, im_name, im_self)
- else:
- raise TypeError("instance method changed")
-
- elif c is Instance:
- klass = reflect.namedObject(ao.klass)
- state = self.unjellyAO(ao.state)
- inst = klass.__new__(klass)
- if hasattr(klass, "__setstate__"):
- self.callAfter(inst.__setstate__, state)
- else:
- inst.__dict__ = 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: _l(*result), loadfunc
- )
- return d
- else:
- raise TypeError("Unsupported AOT type: %s" % t)
-
- def unjelly(self, ao):
- try:
- l = [None]
- self.unjellyInto(l, 0, ao)
- for func, v in self.afterUnjelly:
- func(v[0])
- return l[0]
- except BaseException:
- log.msg("Error jellying object! Stacktrace follows::")
- log.msg("\n".join(map(repr, self.stack)))
- 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).encode("utf-8"))
- else:
- return getSource(aot)
-
-
- def _classOfMethod(methodObject):
- """
- Get the associated class of the given method object.
-
- @param methodObject: a bound method
- @type methodObject: L{types.MethodType}
-
- @return: a class
- @rtype: L{type}
- """
- return methodObject.__self__.__class__
-
-
- def _funcOfMethod(methodObject):
- """
- Get the associated function of the given method object.
-
- @param methodObject: a bound method
- @type methodObject: L{types.MethodType}
-
- @return: the function implementing C{methodObject}
- @rtype: L{types.FunctionType}
- """
- return methodObject.__func__
-
-
- def _selfOfMethod(methodObject):
- """
- Get the object that a bound method is bound to.
-
- @param methodObject: a bound method
- @type methodObject: L{types.MethodType}
-
- @return: the C{self} passed to C{methodObject}
- @rtype: L{object}
- """
- return methodObject.__self__
-
-
- 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 issubclass(objType, 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(
- _funcOfMethod(obj).__name__,
- reflect.qual(_classOfMethod(obj)),
- self.jellyToAO(_selfOfMethod(obj)),
- )
-
- elif issubclass(objType, types.ModuleType):
- retval = Module(obj.__name__)
-
- 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 id(obj) in self.prepared:
- 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()
-
- def _stateFrom(state):
- retval.setObj(
- Instance(reflect.qual(obj.__class__), self.jellyToAO(state))
- )
-
- self.prepareForRef(retval, obj)
-
- if objType is list:
- retval.setObj([self.jellyToAO(o) for o in obj]) # hah!
-
- elif objType is tuple:
- retval.setObj(tuple(map(self.jellyToAO, obj)))
-
- elif objType is dict:
- d = {}
- for k, v in obj.items():
- d[self.jellyToAO(k)] = self.jellyToAO(v)
- retval.setObj(d)
-
- elif objType in copy_reg.dispatch_table:
- unpickleFunc, state = copy_reg.dispatch_table[objType](obj)
-
- retval.setObj(
- Copyreg(reflect.fullFuncName(unpickleFunc), self.jellyToAO(state))
- )
-
- elif hasattr(obj, "__getstate__"):
- _stateFrom(obj.__getstate__())
- elif hasattr(obj, "__dict__"):
- _stateFrom(obj.__dict__)
- 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 BaseException:
- log.msg("Error jellying object! Stacktrace follows::")
- log.msg("\n".join(self.stack))
- raise
|