Thursday, December 08, 2005

Stupid Metaclass and Template Tricks

I've been playing around with various web templating solutions and really like the approach taken with Twisted/Nevow/Stan. Basically, you can create HTML by doing something like this: (taken from this example)

T.html[
T.body[
T.h1['ChildPage'],
T.p['My name is: ', T.span(id="name")[render_name]],
T.p[
'I have child too: ',
T.a(id="child", href=url.here.child('childOfChild'))['my child']
],
]
]
I like the programmatic construction and indentation features. The only problem is that I use emacs, and emacs doesn't like to indent nested Python lists like that. Some Quixote developers were also looking at extending their templating solution to include STAN-like blocks in this thread. I like the general approach, and I certainly like the resulting syntax, but I'm not crazy about using the compiler module and import hooks for web templates.

I've also been looking at using metaclasses for a while. I find them intriguing, and feel that my "Python cred" would be enhanced if I could actually come up with a decent use for them. Ian Bicking has written a few posts recently concerning them, and so I thought I'd try my hand at implementing something like what the Quixote developers are doing, but without using import hooks. (The only black magic here is the use of sys._getframe() and metaclasses.) I'm sure there are plenty of bugs, etc. in the code, but here it is for anyone who'd like to take it and run (or comment on how it could be improved). The actual template is at the bottom. Enjoy!


#!/usr/bin/env python2.4
import sys
from string import Template
from xml.sax import saxutils

def findParentFrame(start_depth = 0):
f = sys._getframe(start_depth+1)
while f.f_back and 'meta' in f.f_locals and issubclass(f.f_locals['meta'], AddToParentContent):
f = f.f_back
return f

class AddToParentContent(type):
def __new__(meta, name, bases, dct):
klass = type.__new__(meta, name, bases, dct)
meta.addParentContent(klass)
return klass
@classmethod
def addParentContent(meta, obj):
f = findParentFrame()
f.f_locals.setdefault('_content', []).append(obj)

class TagMeta(AddToParentContent):
def __new__(meta, name, bases, dct):
dct.setdefault('_content', [])
dct.setdefault('_attrs', {})
klass = AddToParentContent.__new__(meta, name, bases, dct)
if klass.__doc__:
klass._content.insert(0, _text(klass.__doc__))
return klass
def __init__(klass, name, bases, dct):
AddToParentContent.__init__(klass, name, bases, dct)
for k,v in dct.items():
if k.startswith('_'): continue
if isinstance(v, type): continue
klass._attrs[k] = v

class Tag(object):
__metaclass__=TagMeta
def __init__(self, *l, **kw):
self._l, self._kw = l, kw
def __str__(self):
attrs = ' '.join(['%s="%s"' % t for t in self._attrs.items()])
l = [ '<%s %s>' % (self.__class__.__name__, attrs) ]
content = []
for obj in self._content:
if callable(obj): obj = obj(*self._l, **self._kw)
s = str(obj)
s = s.replace('\n', '\n ')
content.append(' ' + s)
l.extend(content)
l.append('</%s>' % self.__class__.__name__)
l = [ s for s in l if s ]
return '\n'.join(l)

def _text(s, escape=True, substitute=True):
tpl = Template(s)
if escape and substitute:
def inst(*l, **kw):
return saxutils.escape(tpl.safe_substitute(*l, **kw))
elif escape:
def inst(*l, **kw): return saxutils.escape(s)
elif substitute:
def inst(*l, **kw): return tpl.safe_substitute(*l, **kw)
else:
def inst(*l, **kw): return s
return inst

def text(s, escape=True, substitute=True):
inst = _text(s, escape, substitute)
f = findParentFrame(1)
f.f_locals.setdefault('_content', []).append(inst)

def eval_(expr, escape=True, substitute=True):
def inst(*l, **kw):
inner = _text(str(eval(expr, kw, kw)), escape, substitute)
return inner(*l, **kw)
f = findParentFrame(1)
f.f_locals.setdefault('_content', []).append(inst)

def foreach(seq):
class _:
__metaclass__=AddToParentContent
def __init__(self, *l, **kw):
self._l, self._kw = l, kw
def __str__(self):
iter_name = self.__class__.__name__
l = []
kw = self._kw.copy()
for i in seq:
kw[iter_name] = i
for obj in self._content:
if callable(obj): obj = obj(*self._l, **kw)
l.append(str(obj))
l = [ s for s in l if s ]
return '\n'.join(l)
return _

def if_(cond):
class _:
__metaclass__=AddToParentContent
def __init__(self, *l, **kw):
self._l, self._kw = l, kw
def __str__(self):
l = []
if eval(cond, self._kw):
for obj in self._content:
if callable(obj): obj = obj(*self._l, **self._kw)
l.append(str(obj))
l = [ s for s in l if s ]
return '\n'.join(l)
return _

class html(Tag):
class head(Tag):
class title(Tag): text('$title')
class body(Tag):
class h1(Tag): text('$title')
class a(Tag): "CNN"; href="http://www.cnn.com"
class table(Tag):
border=1
class i(foreach(range(3))):
class tr(Tag):
class j(foreach(range(3))):
class cond(if_('i <> j')):
class td(Tag): '$i > $j'
class cond(if_('i == j')):
class td(Tag): '$i = $j'

h = html(title="My Page")
print str(h)




Categories: , , , ,

No comments:

Post a Comment