lxmlを使ったpush型テンプレート

webstringが結構よさげだったのだが、少し気になる点があった。

  • 構造化したデータモデルをプッシュするのが少し複雑な気がする。
  • classが指定されているエレメント内のclassを使ってプッシュできない。
  • エレメントの出現順序に依存している部分がある。

もう少しシンプルにならんもんかと、自分で作ってみた。
ポイントは

  • 指定するデータはjsonライクな構造にする。str, unicode, dict, tuple, list を相手にする。
  • 辞書による名前指定は@で始まると属性、それ以外は子孫エレメントを指定するものとする。
  • classとidを特に区別しない。
  • テンプレート差し込みは行わない。
  • tupleの場合は該当エレメントに複数の処理を追加する。
  • listの場合は該当エレメントの子孫エレメントに対する処理を行う。


で、できたのが以下のようなもの。
まだ、例外は適当だし、listとtupleの使い分けがきもいとか、エレメントが使い捨てになるのは大丈夫なのだろうかと不安のある内容だがひとまず晒しておこう。

"""
>>> t = Template()
>>> t.fromstring("<div/>")
>>> str(t)
'<div/>'

>>> t.merge('hello')
>>> str(t)
'<div>hello</div>'

>>> t = Template()
>>> t.fromstring("<div><span class='y'/></div>")
>>> t.merge({'y':['1', '2']})
>>> str(t)
'<div><span class="y">1</span><span class="y">2</span></div>'

>>> t = Template()
>>> t.fromstring("<div><span class='y'/></div>")
>>> t.merge({'y':[({'@id':'m1'}, '1'), 
...               ({'@id':'m2'}, '2')]})
>>> str(t)
'<div><span class="y" id="m1">1</span><span class="y" id="m2">2</span></div>'

>>> t.fromstring("<div><div class='box'><span class='x'/><span class='y'/></div></div>")
>>> t.merge({'box':[{'x':'1', 'y':'2'},
...                 {'x':'3', 'y':'4'}]})
>>> str(t)
'<div><div class="box"><span class="x">1</span><span class="y">2</span></div><div class="box"><span class="x">3</span><span class="y">4</span></div></div>'

>>> t.fromstring("<div/>")
>>> t.merge(ET.Element('span'))
>>> str(t)
'<div><span/></div>'
"""
import lxml.etree as ET
from StringIO import StringIO

class Template(object):
    def __init__(self):
        self.tree = ET.ElementTree()

    def fromfile(self, f):
        self.tree.parse(f)

    def fromstring(self, s):
        f = StringIO(s)
        self.fromfile(f)

    def merge(self, values):
        mergeValues(self.tree.getroot(), values)

    def __str__(self):
        return ET.tostring(self.tree)

def copyTree(tree):
    element = tree.makeelement(tree.tag, tree.attrib)
    element.tail = tree.tail
    element.text = tree.text
    for child in tree:
        element.append(copyTree(child))
    return element

def mergeValues(element, value):
    ltype = type(value)
    if ET.iselement(value):
        element.append(value)
    elif ltype in (str, unicode):
        element.text = value
    elif ltype == dict:
        for k,v in value.iteritems():
            if k.startswith("@"):
                element.set(k[1:], v)
            else:
                children = element.xpath(r".//*[@id='%s']" % k)
                if len(children) == 0:
                    children = element.xpath(r".//*[@class='%s']" % k)
                if len(children) > 0:
                    for child in children:
                        mergeValues(child, v)
                else:
                    raise Exception, "children nodes not found for '%s'." % k
    elif ltype == tuple:
        for v in value:
            mergeValues(element, v)
    elif ltype == list:
        parent = element.getparent()
        parent.remove(element)
        for v in value:
            e = copyTree(element)
            mergeValues(e, v)
            parent.append(e)
    else:
        raise Exception, "Sorry, applicable types are str, unicode, dict, tuple and list."

if __name__ == '__main__':
    import doctest
    doctest.testmod()

TODO
WSGI対応 -> PasteDeploy用のfilter_factoryとentry_point作成