One reasonable approach to this is to use python descriptors to create properties on the object which know how to serialize and deserialize themselves. Descriptors are the mechanism python uses to create the @property decorators: the contain getter and setter methods and can have local state so they make a good staging ground in between your data and your xml. Coupled with a class or decorator that automates the process of bulk serializing/deserializing the descriptors attached to an object, you have the guts of the C# XML serialization system.
Generically, you'd want code to look like this (using the infamous XML ISBN example:
@xmlobject("Book")
class Book( object ):
author = XElement( 'AuthorsText' )
title = XElement( 'Title' )
bookId = XAttrib( 'book_id' )
isbn = IntAttrib( 'isbn' )
publisher = XInstance( 'PublisherText', Publisher )
The assigment syntax here is creating class-level descriptors for what will be all the fields in the instance ( author, title, etc). Each descriptor looks like a regular field to other python code, so you can do things like:
book.author = 'Joyce, James'
and so on. Internally each descriptor stores and xml node or attribute, and when called on to serialize it will return the appropriate XML:
from xml.etree.cElementTree import ElementTree, Element
class XElement( object ):
'''
Simple XML serializable field
'''
def __init__( self, path):
self.path = path
self._xml = Element(path) # using an ElementTree or lxml element as internal storage
def get_xml( self, inst ):
return inst._xml
def _get_element( self ):
return self.path
def _get_attribute( self ):
return None
# the getter and setter push values into the underlying xml and return them from there
def __get__( self, instance, owner=None ):
myxml = self.get_xml( instance )
underlying = myxml.find( self.path )
return underlying.text
def __set__( self, instance, value, owner=None ):
myxml= self._get_xml( instance )
underlying = myxml.find( self.path )
underlying.text = value
The corresponding XAttrib class does the same thing, except in an attribute instead of a an element.
class XAttrib( XElement):
'''
Wraps a property in an attribute on the containing xml tag specified by 'path'
'''
def __get__( self, instance, owner=None ):
return self._get_xml( instance ).attrib[self.path]
# again, using ElementTree under the hood
def __set__( self, instance, value, owner=None ):
myxml = self._get_xml( instance )
has_element = myxml.get( self.path, 'NOT_FOUND' )
if has_element == 'NOT_FOUND':
raise Exception, "instance has no element path"
myxml.set( self.path, value )
def _get_element( self ):
return None #so outside code knows we are an attrib
def _get_attribute( self ):
return self.path
To tie it all together, the owning class needs to set up the descriptors at initialization time so each instance-level descriptor is pointed at an XML node in the owning instance's own XML element. That way the changes to instance props are automatically reflected in the owner's XML.
def create_defaults( target_cls):
# where target class is the serializable class, eg 'Book'
# here _et_xml() would return the class level Element, just
# as in the XElement and XAttribute. Good use for a decorator!
myxml = target_cls.get_xml()
default_attribs = [item for item in target_cls.__class__.__dict__.values()
if issubclass( item.__class__, XElement) ]
#default attribs will be all the descriptors in the target class
for item in default_attribs:
element_name = item._get_element()
#update the xml for the owning class with
# all the XElements
if element_name:
new_element = Element( element_name )
new_element.text = str( item.DEFAULT_VAL )
myxml.append( new_element )
# then update the owning XML with the attributes
for item in default_attribs:
attribpath = item._get_attribute()
if attrib:
myxml.set( attribpath, str( item.DEFAULT_VAL ) )
Apologies if this code doesn't run off the bat - I stripped it down from a working example but i may have introduced bugs while trying to make it readable and removing details specific to my application.