Thursday, February 12, 2009

Writing Xml with IronPython, XmlWriter and the 'with' statement

This post shows a little example of wrapping calls to System.Xml.XmlWriter inside a Python's 'with' statement using IronPython.

Writing Xml



While reading some code examples from the XIST HTML/XML generation library I noticed the nice use of Python's 'with' statement to represent the target HTML or XML.

The System.Xml.XmlWriter class provided with .NET already gives you a way to write well formed Xml documents. In this post I'm going to show how to use an XmlWriter instance in conjunction with Python's 'with' statement.

We want to write the following code:


from __future__ import with_statement

...

w = XmlWriter.Create(System.Console.Out,XmlWriterSettings(Indent=True))
x = XWriter(w)

with x.element('tableofcontents'):
with x.element('section',{'page' : '10'}):
x.text('Introduction')
with x.element('section',{'page' : '12'}):
x.text('Main topic')
with x.element('section',{'page' : '14'}):
x.text('Extra topic')


To generate the following Xml file:


<tableofcontents>
<section page="10">Introduction</section>
<section page="12">Main topic</section>
<section page="14">Extra topic</section>
</tableofcontents>


The 'with' statement



The 'with' statement was introduced in Python 2.5 . This statement is used wrap the execution of a series of statements with some special code. For example it is used to implement the try...except...finally pattern.

As described in the documentation the following statement:


with context expression:
statements...


Will be executed as follows:


  1. Evaluate the context expression to obtain the context manager

  2. Invoke the context manager's __enter__() method

  3. Execute the statements

  4. When the execution of the statements finishes(even with an exception), the context manager's __exit()__ method is called.



Given these steps we're going to implement a context manager that assist in the creation of Xml documents using the System.Xml.XmlWriter .NET class.

The following code shows a class that wraps the XmlWriter instance and helps with the creation of context managers:


class XWriter(object):
def __init__(self,writer):
self.writer = writer

def element(self,name,atts = {}):
return ElementCtxt(name,atts,self)

def nselement(self,prefix,name,namespace,atts = {}):
return NamespaceElementCtxt(prefix,name,namespace,atts,self)


def text(self,text):
self.writer.WriteString(text)

def cdata(self,text):
self.writer.WriteCData(text)



Notice that the element method creates an instance of the ElementCtxt class using the element name and an optional dictionary with the attributes. As the following listing shows this class performs the calls to WriteStartElement and WriteEndElement in the __enter__ and __exit__ methods.


class ElementCtxt(object):
def __init__(self,elementName,atts,writer):
self.elementName = elementName
self.atts = atts
self.writer = writer

def processAttributes(self):
for att in self.atts:
self.writer.writer.WriteAttributeString(att,self.atts[att].__str__())

def processStartTag(self):
self.writer.writer.WriteStartElement(self.elementName)
self.processAttributes()

def __enter__(self):
self.processStartTag()
return self

def __exit__(self,t,v,tr):
self.writer.writer.WriteEndElement()
return t == None



The XWriter.nselement method is used to write elements with namespace and prefix. This call generates an instance of the following context manager:


class NamespaceElementCtxt(ElementCtxt):
def __init__(self,prefix,elementName,namespace,atts,writer):
ElementCtxt.__init__(self,elementName,atts,writer)
self.namespace = namespace
self.prefix = prefix
def processStartTag(self):
self.writer.writer.WriteStartElement(self.prefix,self.elementName,self.namespace)
self.processAttributes()


Final example



The following code shows how to create a little SVG file:


from __future__ import with_statement
from xmlwriterw import XWriter

import clr

clr.AddReference('System.Xml')

from System.Xml import *
import System


w = XmlWriter.Create(System.Console.Out,\
XmlWriterSettings(Indent=True))
x = XWriter(w)

svgNs = 'http://www.w3.org/2000/svg'

with x.nselement('s','svg',svgNs,{'version': '1.1',
'viewBox': '0 0 100 100',
'style':'width:100%; height:100%; position:absolute; top:0; left:0; z-index:-1;'}):
with x.nselement('s','linearGradient',svgNs, { 'id' : 'gradient' }):
with x.nselement('s','stop',svgNs, {'class' : 'begin',
'offset' : '0%',
'stop-color':'red'}):
pass
with x.nselement('s','stop',svgNs, {'class' : 'end',
'offset' : '100%'}):
pass
with x.nselement('s','rect',svgNs, { 'x':0,
'y':0,
'width':100,
'height':100,
'style':'fill:url(#gradient)'} ):
pass
for i in range(1,5):
with x.nselement('s','circle',svgNs, { 'cx': 50,
'cy': 50,
'r': 30 - i*3,
'style':'fill:url(#gradient)'} ):
pass

w.Close()


Running this program shows:


<s:svg viewBox="0 0 100 100" style="width:100%; height:100%; position:absolute; top:0; left:0; z-index:-1;" version="1.1" xmlns:s="http://www.w3.org/2000/svg">
<s:linearGradient id="gradient">
<s:stop offset="0%" class="begin" stop-color="red" />
<s:stop offset="100%" class="end" />
</s:linearGradient>
<s:rect x="0" height="100" width="100" style="fill:url(#gradient)" y="0" />
<s:circle cx="50" cy="50" style="fill:url(#gradient)" r="27" />
<s:circle cx="50" cy="50" style="fill:url(#gradient)" r="24" />
<s:circle cx="50" cy="50" style="fill:url(#gradient)" r="21" />
<s:circle cx="50" cy="50" style="fill:url(#gradient)" r="18" />
</s:svg>