We're back after a server migration that caused effbot.org to fall over a bit harder than expected. Expect some glitches.

An ElementTree Builder

Fredrik Lundh | November 2006 | Originally posted to online.effbot.org

Here’s a simple helper class for ElementTree, which lets you use a more convenient syntax to construct XML fragment trees:

import xml.etree.cElementTree as ET
import functools

class _E(object):

    def __call__(self, tag, *children, **attrib):
        elem = ET.Element(tag, attrib)
        for item in children:
            if isinstance(item, dict):
                elem.attrib.update(item)
            elif isinstance(item, basestring):
                if len(elem):
                    elem[-1].tail = (elem[-1].tail or "") + item
                else:
                    elem.text = (elem.text or "") + item
            elif ET.iselement(item):
                elem.append(item)
            else:
                raise TypeError("bad argument: %r" % item)
        return elem

    def __getattr__(self, tag):
        return functools.partial(self, tag)

# create factory object
E = _E()

This started out as a small tweak to the append helper function, but quickly turned into something more like Oren Tirosh’ ElementBuilder (which is one of all those tools that I’ve stumbled upon, found interesting, but never gotten around to use).

Anyway, unlike the ordinary Element factory, the E factory allows you to pass in more than just a tag and some optional attributes; you can also pass in text and other elements. The text is added as either text or tail attributes, and elements are inserted at the right spot. Some small examples:

>>> ET.tostring(E("tag"))
'<tag />'
>>> ET.tostring(E("tag", "text"))
'<tag>text</tag>'
>>> ET.tostring(E("tag", "text", key="value"))
'<tag key="value">text</tag>'
>>> ET.tostring(E("tag", E("subtag", "text"), "tail"))
'<tag><subtag>text</subtag>tail</tag>'

For simple tags, the factory also allows you to write “E.tag(…)” instead of “E(‘tag’, …)”:

>>> ET.tostring(E.tag())
'<tag />'
>>> ET.tostring(E.tag("text"))
'<tag>text</tag>'
>>> ET.tostring(E.tag(E.subtag("text"), "tail"))
'<tag><subtag>text</subtag>tail</tag>'

Here’s a somewhat larger example; this shows how to generate HTML documents, using a mix of prepared factory functions for inline elements, nested E.tag calls, and embedded XHTML fragments:

# some common inline elements
A = E.a
I = E.i
B = E.b

def CLASS(*args):
    # class is a reserved word, so we use a helper function
    return {"class": " ".join(args)}

page = (
  E.html(
    E.head(
      E.title("This is a sample document")
    ),
    E.body(
      E.h1("Hello!", CLASS("title")),
      E.p("This is a paragraph with ", B("bold"), " text in it!"),
      E.p("This is another paragraph, with a ",
          A("link", href="http://www.python.org"), "."),
      E.p("Here are some reservered characters: <spam&egg>."),
      ET.XML("<p>And finally, here's an embedded XHTML fragment.</p>"),
    )
  )
)

print ET.tostring(page)

Here’s a prettyprinted version of the output from the above script:

<html>
  <head>
    <title>This is a sample document</title>
  </head>
  <body>
    <h1 class="title">Hello!</h1>
    <p>This is a paragraph with <b>bold</b> text in it!</p>
    <p>This is another paragraph, with a <a href="http://www.python.org">link</a>.</p>
    <p>Here are some reservered characters: &lt;spam&amp;egg&gt;.</p>
    <p>And finally, here's an embedded XHTML fragment.</p>
  </body>
</html>

I’m still experimenting with more mechanisms, including support for callables and nested sequences, but the above makes many ElementTree-based generation tasks a lot simpler.

Addendum

Tweaking, tweaking. This one’s pretty neat, I think:

from datetime import datetime

RSS = ElementBuilder(typemap={
    datetime: lambda e, v: v.strftime("%a, %d %b %Y %H:%M:%S %z")
    })

item = RSS.item(
    RSS.title("E=RSS squared"),
    RSS.link("http://effbot.org"),
    RSS.description("Yet another E-related example"),
    RSS.pubDate(datetime.now(UTC()))
    )

Code will be posted later. Stay tuned.