import types import pickle from time import time as now from StringIO import StringIO from twisted.python import components from twisted.python.reflect import qual from twisted.web import resource, server class Proto(str): """Proto is a string subclass. Instances of Proto, which are constructed with a string, will construct Tag instances in response to __call__ and __getitem__, delegating responsibility to the tag. """ def __call__(self, **kw): return Tag(self)(**kw) def __getitem__(self, children): return Tag(self)[children] class xml(str): """Raw xml marker """ class directive(str): """Marker for a directive in a template """ def __repr__(self): return "directive('%s')" % self class Tag(object): """Tag instances represent html tags with a tag name, attributes, and children. Tag instances can be constructed using the Prototype tags in the 'tags' module, or may be constructed directly with a tag name. Tags have two special methods, __call__ and __getitem__, which make representing trees of XML natural using pure python syntax. See the docstrings for these methods for more details. """ specials = ['data', 'renderer', 'observer', 'pattern', 'slot'] produceContext = False def __init__(self, tag, attributes=None, children=None, specials=None): self.tagName = tag if attributes is None: self.attributes = {} else: self.attributes = attributes if children is None: self.children = [] else: self.children = children if specials is None: self._specials = {} else: self._specials = specials def __call__(self, **kw): """Change attributes of this tag. This is implemented using __call__ because it then allows the natural syntax: table(width="100%", height="50%", border="1") Three magic attributes may have values other than string values, and will be remembered in the context stack for later retrieval: data renderer observer See WovenContext.with for details. pattern and slot attributes are special, and are not exported to the final page, but are used to locate nodes tagged with a given pattern or slot id. """ if not kw: return self for name in self.specials: if kw.has_key(name): setattr(self, name, kw[name]) del kw[name] for k, v in kw.items(): if k[0] == '_': k = k[1:] assert isinstance(v, str), "Non-special attribute values must be strings; %s found." % v self.attributes[k] = v return self def __getitem__(self, children): """Add children to this tag. Multiple children may be added by passing a tuple or a list. Children may be other tag instances, strings, functions, or any object which has a registered ISerializable adapter. This is implemented using __getitem__ because it then allows the natural syntax: html[ head[ title["Hello World!"] ], body[ "This is a page", h3["How are you!"], div(style="color: blue")["I hope you are fine."] ] ] """ if not isinstance(children, (list, tuple)): children = [children] self.children.extend(children) return self def clone(self, deep=True): """Return a clone of this tag. If deep is True, clone all of this tag's children. Otherwise, the children list of the clone will be empty. """ if deep: newchildren = [ hasattr(ch, 'clone') and ch.clone() or ch for ch in self.children ] else: newchildren = [] return Tag(self.tagName, attributes=self.attributes.copy(), children=newchildren, specials=self._specials.copy()) def clear(self): """Clear any existing children from this tag. """ self.children = [] def __repr__(self): return "Tag(%r, attributes=%r, children=%s, specials=%r)" % (self.tagName, self.attributes, self.children, self._specials) def makeAccessors(special): def getSpecial(self): return self._specials.get(special) def setSpecial(self, data): self._specials[special] = data return getSpecial, setSpecial for name in Tag.specials: setattr(Tag, name, property(*makeAccessors(name))) class TagInContext(object): def __init__(self, tag, context): self.tag = tag self.context = context def __call__(self, **kw): self.tag(**kw) def __getitem__(self, item): self.tag[item] def __getattr__(self, name): return getattr(self.tag, name) def __repr__(self): children = self.tag.children and ", children=%r" % self.tag.children or '' return "TagInContext(%r, specials=%s%s)" % (self.tag.tagName, self.tag._specials, children) class ISerializable(components.Interface): def serialize(self, context, stream): """Serialize the adaptee to the given stream, with the given context stack if necessary. """ class ProtoSerializer(components.Adapter): __implements__ = ISerializable, def serialize(self, context, stream): yield '<%s />' % self.original components.registerAdapter(ProtoSerializer, Proto, ISerializable) class TagSerializer(components.Adapter): __implements__ = ISerializable, def serialize(self, context, stream): visible = bool(self.original.tagName) singleton = not self.original.renderer and not self.original.children and not self.original.data special = context.precompile and self.original._specials if special: context = context.with(self.original, data=self.original.data, renderer=self.original.renderer, observer=self.original.observer) context.tag.children = flatten(ISerializable(context.tag.children).serialize(context, stream)) yield TagInContext(context.tag, context) else: if visible: yield '<%s' % self.original.tagName if self.original.attributes: yield ' ' yield ' '.join(['%s="%s"' % (k, v) for (k, v) in self.original.attributes.items()]) if singleton: if visible: yield ' />' else: if visible: yield '>' # TODO: Make this less buggy. if context.locate(IData) != self.original.data: context = context.with(self.original, data=self.original.data, renderer=self.original.renderer, observer=self.original.observer) if self.original.renderer: toBeRenderedBy = self.original.renderer self.original.renderer = None yield ISerializable(toBeRenderedBy).serialize(context, stream) self.original.wasRenderedBy = toBeRenderedBy elif self.original.children: for child in self.original.children: yield ISerializable(child).serialize(context, stream) elif self.original.data: yield ISerializable(self.original.data).serialize(context, stream) if visible: yield '' components.registerAdapter(TagSerializer, Tag, ISerializable) class StringSerializer(components.Adapter): __implements__ = ISerializable, def serialize(self, context, stream): ## quote it yield self.original components.registerAdapter(StringSerializer, str, ISerializable) components.registerAdapter(StringSerializer, unicode, ISerializable) class StringCastSerializer(components.Adapter): __implements__ = ISerializable, def serialize(self, context, stream): ## quote it yield str(self.original) components.registerAdapter(StringCastSerializer, int, ISerializable) components.registerAdapter(StringCastSerializer, float, ISerializable) components.registerAdapter(StringCastSerializer, long, ISerializable) components.registerAdapter(StringCastSerializer, bool, ISerializable) class ListSerializer(components.Adapter): __implements__ = ISerializable, def serialize(self, context, stream): for item in self.original: yield ISerializable(item).serialize(context, stream) components.registerAdapter(ListSerializer, list, ISerializable) components.registerAdapter(ListSerializer, tuple, ISerializable) class XmlSerializer(components.Adapter): __implements__ = ISerializable, def serialize(self, context, stream): yield self.original components.registerAdapter(XmlSerializer, xml, ISerializable) class FunctionSerializer(components.Adapter): __implements__ = ISerializable, def serialize(self, context, stream): if context.precompile: yield self.original else: data = convertToData(context, context.locate(IData)) code = getattr(self.original, 'func_code', None) nocontext = code is None or code.co_argcount == 1 if nocontext: result = self.original(data) else: result = self.original(context, data) yield ISerializable(result).serialize(context, stream) components.registerAdapter(FunctionSerializer, types.FunctionType, ISerializable) components.registerAdapter(FunctionSerializer, type(str), ISerializable) class TagInContextSerializer(components.Adapter): __implements__ = ISerializable, def serialize(self, context, stream): originalContext = self.original.context tag = self.original.tag originalContext.precompile = context.precompile yield flatten(ISerializable(tag).serialize(originalContext, stream)) components.registerAdapter(TagInContextSerializer, TagInContext, ISerializable) class IData(components.Interface): """Any python object to be used as model data to be passed to view functions. Used for marking the context stack only. ANY python object is said to implement IData. """ class IGettable(components.Interface): def get(self, context): """Return the data Return any object """ class ISettable(components.Interface): def set(self, context, data): """Set the data This might be removed. """ class IContainer(components.Interface): def child(self, context, name): """Return a conceptual child; an attribute, or a key, or the result of a function call. Returns any object; the result may be adapted to IGettable if possible. Return None if the adaptee does not have a child with the given name. TODO: Maybe returning None is bad, and .child should just raise whatever exception is natural """ def convertToData(context, obj): adapter = IGettable(obj, None, persist=False) if adapter is not None: obj = adapter.get(context) return obj class DirectiveAccessor(components.Adapter): __implements__ = IGettable, def get(self, context): # The directive (self.original) is the data at depth=1 data = context.locate(IData, depth=2) container = IContainer(data, None, persist=False) if container is None: raise RuntimeError, "%s does not implement IContainer, and there is no registered adapter." % data child = container.child(context, self.original) return convertToData(context, child) components.registerAdapter(DirectiveAccessor, directive, IGettable) class FunctionAccessor(components.Adapter): __implements__ = IGettable, def get(self, context): # The function (self.original) is the data at depth=1 return self.original(context, context.locate(IData, depth=2)) components.registerAdapter(FunctionAccessor, types.FunctionType, IGettable) class DictionaryContainer(components.Adapter): __implements__ = IContainer, IGettable def child(self, context, name): return self.original.get(name, None) def get(self, context): return dict( [(k, convertToData(context, v)) for (k, v) in self.original.items()] ) components.registerAdapter(DictionaryContainer, types.DictType, IContainer) components.registerAdapter(DictionaryContainer, types.DictType, IGettable) class ListContainer(components.Adapter): __implements__ = IContainer, IGettable def child(self, context, name): try: i = int(name) except ValueError: return None return self.original[i] def get(self, context): return [convertToData(context, x) for x in self.original] components.registerAdapter(ListContainer, types.ListType, IContainer) components.registerAdapter(ListContainer, types.TupleType, IGettable) def specialMatches(tag, special, pattern): """Generate special attribute matches starting with the given tag; if a match is found, do not look any deeper below that match. """ if getattr(tag, special, None) == pattern: yield tag else: for child in getattr(tag, 'children', []): for match in specialMatches(child, special, pattern): yield match class WovenContext(object): cloned = False def __init__(self, parent=None, tag=None, precompile=False): self.tag = tag self.parent = parent self._rememberances = {} self.precompile = precompile def __repr__(self): return "Context(_rememberances=%r)" % self._rememberances def remember(self, adapter, interface=None): """Remember an object that implements some interfaces. Later, calls to .locate which are passed an interface implemented by this object will return this object. If the 'interface' argument is supplied, this object will only be remembered for this interface, and not any of the other interfaces it implements. """ if interface is None: interfaceList = components.getInterfaces(adapter) if not interfaceList: interfaceList = [IData] else: interfaceList = [interface] for interface in interfaceList: self._rememberances[qual(interface)] = adapter return self def with(self, tag, data=None, renderer=None, observer=None): """Remember a few things in a new context stack entry. tag: the tag the new context stack entry will be associated with. data: The data to pass to any rendering functions. renderer: The rendering function to use to render this node. observer: The observer function to notify when an event occurs to this node. """ new = WovenContext(self, tag, self.precompile) if data is not None: new.remember(data, IData) if renderer is not None: # push a renderer onto the stack pass if observer is not None: # push an observer onto the stack pass return new def locate(self, interface, depth=1): """Locate an object which implements a given interface. Objects will be searched through the context stack top down. """ key = qual(interface) if self.parent is None: return None if self._rememberances.has_key(key): depth -= 1 if not depth: return self._rememberances[key] return self.parent.locate(interface, depth) def generatePatterns(self, pattern): """Generate clones of pattern tags forever, looping around to the beginning when we run out of unique matches. """ keeplooking = True while keeplooking: keeplooking = False for x in specialMatches(self.tag, 'pattern', pattern): keeplooking = True yield x.clone() raise RuntimeError, "Pattern %s was not found." % pattern def locateSlots(self, slot): """Locate and return an existing slot in the current context tag, clearing children in the process. This will cause the existing tag to be cloned to avoid disturbing the template; you could return context.tag or another tag after calling this. """ return specialMatches(self.tag, 'slot', slot) def flatten(gen): results = [] accumulator = '' for item in gen: if isinstance(item, types.StringTypes): accumulator += item else: if isinstance(item, types.GeneratorType): for sub in flatten(item): if isinstance(sub, types.StringTypes): accumulator += sub else: if accumulator: results.append(accumulator) results.append(sub) accumulator = '' elif isinstance(item, types.ListType): if accumulator: results.append(accumulator) results.extend(item) accumulator='' else: if accumulator: results.append(accumulator) results.append(item) accumulator = '' if accumulator: results.append(accumulator) return results from copy import deepcopy class Renderer(resource.Resource): def __init__(self, doc): context = WovenContext(precompile=True).remember(self, resource.IResource) self.doc = doc # uncomment this... #self.doc = flatten(ISerializable(doc).serialize(context, None)) from pprint import pprint pprint(self.doc) resource.Resource.__init__(self) def getChild(self, name, request): if name == '': return self return resource.Resource.getChild(self, name, request) def render(self, request): context = WovenContext().remember(self, resource.IResource) start = now() # and comment this... doc = self.doc.clone() # and uncomment this... # doc = self.doc result = flatten(ISerializable(doc).serialize(context, request)) print "TIME", now() - start request.write(result[0]) request.finish() return server.NOT_DONE_YET