1 #----------------------------------------------------------------------------
2 # Name: xmlmarshaller.py
5 # Author: John Spurling
9 # Copyright: (c) 2004-2005 ActiveGrid, Inc.
10 # License: wxWindows License
11 #----------------------------------------------------------------------------
12 from activegrid
import util
16 import xml
.sax
.handler
18 from xml
.sax
import saxutils
20 ### ToDO remove maxOccurs "unbounded" resolves to -1 hacks after bug 177 is fixed
24 More documentation later, but here are some special Python attributes
25 that McLane recognizes:
29 description: the name of the xml element for the marshalled object
31 name: __xmlattributes__
33 description: the name(s) of the Python string attribute(s) to be
34 marshalled as xml attributes instead of nested xml elements. currently
35 these can only be strings since there's not a way to get the type
36 information back when unmarshalling.
40 description: the name(s) of the python attribute(s) to skip when
45 description: describes an alternate Python <-> XML name mapping.
46 Normally the name mapping is the identity function. __xmlrename__
47 overrides that. The keys are the Python names, the values are their
50 name: __xmlflattensequence__
51 type: dict, tuple, or list
52 description: the name(s) of the Python sequence attribute(s) whose
53 items are to be marshalled as a series of xml elements (with an
54 optional keyword argument that specifies the element name to use) as
55 opposed to containing them in a separate sequence element, e.g.:
58 <!-- normal way of marshalling -->
60 <item objtype='int'>1</item>
61 <item objtype='int'>2</item>
63 <!-- with __xmlflattensequence__ set to {'myseq': 'squish'} -->
64 <squish objtype='int'>1</squish>
65 <squish objtype='int'>2</squish>
67 name: __xmlnamespaces__
69 description: a dict of the namespaces that the object uses. Each item
70 in the dict should consist of a prefix,url combination where the key is
71 the prefix and url is the value, e.g.:
73 __xmlnamespaces__ = { "xsd":"http://www.w3c.org/foo.xsd" }
75 name: __xmldefaultnamespace__
77 description: the prefix of a namespace defined in __xmlnamespaces__ that
78 should be used as the default namespace for the object.
80 name: __xmlattrnamespaces__
82 description: a dict assigning the Python object's attributes to the namespaces
83 defined in __xmlnamespaces__. Each item in the dict should consist of a
84 prefix,attributeList combination where the key is the prefix and the value is
85 a list of the Python attribute names. e.g.:
87 __xmlattrnamespaces__ = { "ag":["firstName", "lastName", "addressLine1", "city"] }
92 ################################################################################
96 ################################################################################
98 class Error(Exception):
99 """Base class for errors in this module."""
102 class UnhandledTypeException(Error
):
103 """Exception raised when attempting to marshal an unsupported
106 def __init__(self
, typename
):
107 self
.typename
= typename
109 return "%s is not supported for marshalling." % str(self
.typename
)
111 class XMLAttributeIsNotStringType(Error
):
112 """Exception raised when an object's attribute is specified to be
113 marshalled as an XML attribute of the enclosing object instead of
116 def __init__(self
, attrname
, typename
):
117 self
.attrname
= attrname
118 self
.typename
= typename
120 return """%s was set to be marshalled as an XML attribute
121 instead of a nested element, but the object's type is %s, not
122 string.""" % (self
.attrname
, self
.typename
)
124 ################################################################################
128 ################################################################################
131 XMLNS_PREFIX
= XMLNS
+ ':'
132 XMLNS_PREFIX_LENGTH
= len(XMLNS_PREFIX
)
134 BASETYPE_ELEMENT_NAME
= 'item'
135 MEMBERS_TO_SKIP
= ('__module__', '__doc__', '__xmlname__', '__xmlattributes__',
136 '__xmlexclude__', '__xmlflattensequence__', '__xmlnamespaces__',
137 '__xmldefaultnamespace__', '__xmlattrnamespaces__')
139 WELL_KNOWN_OBJECTS
= { "xs:element" : "activegrid.model.schema.XsdElement",
140 "xs:complexType" : "activegrid.model.schema.XsdComplexType",
141 "xs:complexType" : "activegrid.model.schema.XsdComplexType",
142 "xs:element" : "activegrid.model.schema.XsdElement",
143 "xs:key" : "activegrid.model.schema.XsdKey",
144 "xs:field" : "activegrid.model.schema.XsdKeyField",
145 "xs:keyref" : "activegrid.model.schema.XsdKeyRef",
146 "xs:selector" : "activegrid.model.schema.XsdKeySelector",
147 "xs:schema" : "activegrid.model.schema.Schema",
148 "ag:schemaOptions":"activegrid.model.schema.SchemaOptions",
149 "ag:debug" : "activegrid.model.processmodel.DebugOperation",
153 ################################################################################
155 # classes and functions
157 ################################################################################
159 def _objectfactory(objname
, objargs
=None, xsname
=None):
161 '''dynamically create an object based on the objname and return
162 it. look it up in the BASETYPE_ELEMENT_MAP first.
164 ## print "_objectfactory creating an object of type %s and value %s, xsname=%s" % (objname, objargs, xsname)
165 # split the objname into the typename and module path,
166 # importing the module if need be.
167 if not isinstance(objargs
, list):
172 objname
= WELL_KNOWN_OBJECTS
[xsname
]
176 objtype
= objname
.split('.')[-1]
177 pathlist
= objname
.split('.')
178 modulename
= '.'.join(pathlist
[0:-1])
180 ## print "[objectfactory] objtype is %s" % objtype
181 ## print "[objectfactory] objargs is %s" % `objargs`
183 ## since the bool constructor will turn a string of non-zero
184 ## length into True, we call it with no argument (yielding a
185 ## False) if the string contains 'false'
186 if objtype
== 'bool' and objargs
[0].lower() == 'false':
189 ## if objtype == 'str':
190 ## print type(objargs)
191 ## print "string we're unescaping: '%s'" % objargs[0]
192 ## objargs = saxutils.unescape(objargs[0])
193 if objtype
in ('float', 'int', 'str', 'long'):
194 objargs
= [x
.strip() for x
in objargs
]
197 objargs
= [saxutils
.unescape(x
) for x
in objargs
]
199 if __builtin__
.__dict
__.has_key(objname
):
203 module
= __import__(modulename
)
204 for name
in pathlist
[1:-1]:
205 module
= module
.__dict
__[name
]
207 return module
.__dict
__[objtype
](*objargs
)
209 if objtype
== 'None':
211 return module
.__dict
__[objtype
]()
213 raise KeyError("Could not find class %s" % objname
)
216 def __init__(self
, name
, attrs
=None):
221 def getobjtype(self
):
222 if self
.attrs
.has_key('objtype'):
223 return self
.attrs
.getValue('objtype')
228 class XMLObjectFactory(xml
.sax
.ContentHandler
):
230 self
.rootelement
= None
231 self
.elementstack
= []
232 xml
.sax
.handler
.ContentHandler
.__init
__(self
)
234 ## ContentHandler methods
235 def startElement(self
, name
, attrs
):
236 if name
.find(':') > -1: # Strip namespace prefixes for now until actually looking them up in xsd
237 name
= name
[name
.index(':') + 1:]
238 ## for attrname in attrs.getNames():
239 ## print "%s: %s" % (attrname, attrs.getValue(attrname))
240 element
= Element(name
, attrs
.copy())
241 self
.elementstack
.append(element
)
242 ## print self.elementstack
244 def characters(self
, content
):
245 ## print "got content: %s" % content
247 self
.elementstack
[-1].content
+= content
249 def endElement(self
, name
):
250 ## print "[endElement] name of element we're at the end of: %s" % name
252 if name
.find(':') > -1: # Strip namespace prefixes for now until actually looking them up in xsd
253 name
= name
[name
.index(':') + 1:]
254 element
= self
.elementstack
.pop()
255 objtype
= element
.getobjtype()
256 constructorarglist
= []
258 strippedElementContent
= element
.content
.strip()
259 if strippedElementContent
:
260 constructorarglist
.append(element
.content
)
261 obj
= _objectfactory(objtype
, constructorarglist
, xsname
)
263 if hasattr(obj
, '__xsdcomplextype__'):
264 complexType
= getattr(obj
, '__xsdcomplextype__')
265 if len(self
.elementstack
) > 0:
266 self
.elementstack
[-1].children
.append((name
, obj
))
268 self
.rootelement
= obj
269 if element
.attrs
and not isinstance(obj
, list):
270 for attrname
, attr
in element
.attrs
.items():
271 if attrname
== XMLNS
or attrname
.startswith(XMLNS_PREFIX
):
272 if attrname
.startswith(XMLNS_PREFIX
):
273 ns
= attrname
[XMLNS_PREFIX_LENGTH
:]
276 if not hasattr(obj
, '__xmlnamespaces__'):
277 obj
.__xmlnamespaces
__ = {ns:attr}
278 elif ns
not in obj
.__xmlnamespaces
__:
279 if (hasattr(obj
.__class
__, '__xmlnamespaces__')
280 and obj
.__xmlnamespaces
__ is obj
.__class
__.__xmlnamespaces
__):
281 obj
.__xmlnamespaces
__ = dict(obj
.__xmlnamespaces
__)
282 obj
.__xmlnamespaces
__[ns
] = attr
283 elif not attrname
== 'objtype':
284 if attrname
.find(':') > -1: # Strip namespace prefixes for now until actually looking them up in xsd
285 attrname
= attrname
[attrname
.index(':') + 1:]
287 xsdElement
= complexType
.findElement(attrname
)
289 type = xsdElement
.type
291 type = xsdToPythonType(type)
292 ### ToDO remove maxOccurs hack after bug 177 is fixed
293 if attrname
== "maxOccurs" and attr
== "unbounded":
295 attr
= _objectfactory(type, attr
)
296 util
.setattrignorecase(obj
, _toAttrName(obj
, attrname
), attr
)
297 ## obj.__dict__[_toAttrName(obj, attrname)] = attr
298 # stuff any child attributes meant to be in a sequence via the __xmlflattensequence__
300 if hasattr(obj
, '__xmlflattensequence__'):
301 for sequencename
, xmlnametuple
in obj
.__xmlflattensequence
__.items():
302 for xmlname
in xmlnametuple
:
303 flattenDict
[xmlname
] = sequencename
305 # reattach an object's attributes to it
306 for childname
, child
in element
.children
:
307 if flattenDict
.has_key(childname
):
308 sequencename
= _toAttrName(obj
, flattenDict
[childname
])
310 sequencevalue
= obj
.__dict
__[sequencename
]
311 except AttributeError:
313 if sequencevalue
== None:
315 obj
.__dict
__[sequencename
] = sequencevalue
316 sequencevalue
.append(child
)
317 elif isinstance(obj
, list):
320 ## print "childname = %s, obj = %s, child = %s" % (childname, repr(obj), repr(child))
321 util
.setattrignorecase(obj
, _toAttrName(obj
, childname
), child
)
322 ## obj.__dict__[_toAttrName(obj, childname)] = child
325 for element
in complexType
.elements
:
327 elementName
= _toAttrName(obj
, element
.name
)
328 if ((elementName
not in obj
.__dict
__) or (obj
.__dict
__[elementName
] == None)):
329 pythonType
= xsdToPythonType(element
.type)
330 defaultValue
= _objectfactory(pythonType
, element
.default
)
331 obj
.__dict
__[elementName
] = defaultValue
333 def getRootObject(self
):
334 return self
.rootelement
336 def _toAttrName(obj
, name
):
337 if (hasattr(obj
, "__xmlrename__")):
338 for key
, val
in obj
.__xmlrename
__.iteritems():
342 ## if (name.startswith("__") and not name.endswith("__")):
343 ## name = "_%s%s" % (obj.__class__.__name__, name)
346 __typeMappingXsdToPython
= {
350 "date": "str", # ToDO Need to work out how to create python date types
352 "decimal": "float", # ToDO Does python have a better fixed point type?
358 "unicode": "unicode",
361 def xsdToPythonType(xsdType
):
363 return __typeMappingXsdToPython
[xsdType
]
365 raise Exception("Unknown xsd type %s" % xsdType
)
367 def _getXmlValue(pythonValue
):
368 if (isinstance(pythonValue
, bool)):
369 return str(pythonValue
).lower()
371 return str(pythonValue
)
373 def unmarshal(xmlstr
):
374 objectfactory
= XMLObjectFactory()
375 xml
.sax
.parseString(xmlstr
, objectfactory
)
376 return objectfactory
.getRootObject()
379 def marshal(obj
, elementName
=None, nameSpacePrefix
='', nameSpaces
=None, prettyPrint
=False, indent
=0):
380 if prettyPrint
or indent
:
389 ## Determine the XML element name. If it isn't specified in the
390 ## parameter list, look for it in the __xmlname__ Python
391 ## attribute, else use the default generic BASETYPE_ELEMENT_NAME.
392 if not nameSpaces
: nameSpaces
= {} # Need to do this since if the {}
is a default parameter it gets shared by all calls into the function
394 if hasattr(obj
, '__xmlnamespaces__'):
395 for nameSpaceKey
, nameSpaceUrl
in getattr(obj
, '__xmlnamespaces__').items():
396 if nameSpaceUrl
in nameSpaces
:
397 nameSpaceKey
= nameSpaces
[nameSpaceUrl
]
399 ## # TODO: Wait to do this until there is shared state for use when going through the object graph
400 ## origNameSpaceKey = nameSpaceKey # Make sure there is no key collision, ie: same key referencing two different URL's
402 ## while nameSpaceKey in nameSpaces.values():
403 ## nameSpaceKey = origNameSpaceKey + str(i)
405 nameSpaces
[nameSpaceUrl
] = nameSpaceKey
406 if nameSpaceKey
== '':
407 nameSpaceAttrs
+= ' xmlns="%s" ' % (nameSpaceUrl
)
409 nameSpaceAttrs
+= ' xmlns:%s="%s" ' % (nameSpaceKey
, nameSpaceUrl
)
410 nameSpaceAttrs
= nameSpaceAttrs
.rstrip()
411 if hasattr(obj
, '__xmldefaultnamespace__'):
412 nameSpacePrefix
= getattr(obj
, '__xmldefaultnamespace__') + ':'
414 if hasattr(obj
, '__xmlname__'):
415 elementName
= nameSpacePrefix
+ obj
.__xmlname
__
417 elementName
= nameSpacePrefix
+ BASETYPE_ELEMENT_NAME
419 elementName
= nameSpacePrefix
+ elementName
422 ## Add more members_to_skip based on ones the user has selected
423 ## via the __xmlexclude__ attribute.
424 if hasattr(obj
, '__xmlexclude__'):
425 members_to_skip
+= list(obj
.__xmlexclude
__)
426 # Marshal the attributes that are selected to be XML attributes.
428 className
= obj
.__class
__.__name
__
429 classNamePrefix
= "_" + className
430 if hasattr(obj
, '__xmlattributes__'):
431 xmlattributes
= obj
.__xmlattributes
__
432 members_to_skip
+= xmlattributes
433 for attr
in xmlattributes
:
434 internalAttrName
= attr
435 if (attr
.startswith("__") and not attr
.endswith("__")):
436 internalAttrName
= classNamePrefix
+ attr
437 # Fail silently if a python attribute is specified to be
438 # an XML attribute but is missing.
440 value
= obj
.__dict
__[internalAttrName
]
443 ## # But, check and see if it is a property first:
444 ## if (hasPropertyValue(obj, attr)):
445 ## value = getattr(obj, attr)
449 if hasattr(obj
, '__xsdcomplextype__'):
450 complexType
= getattr(obj
, '__xsdcomplextype__')
451 xsdElement
= complexType
.findElement(attr
)
453 default
= xsdElement
.default
454 if default
== value
or default
== _getXmlValue(value
):
459 # ToDO remove maxOccurs hack after bug 177 is fixed
460 if attr
== "maxOccurs" and value
== -1:
463 if isinstance(value
, bool):
469 attrNameSpacePrefix
= ''
470 if hasattr(obj
, '__xmlattrnamespaces__'):
471 for nameSpaceKey
, nameSpaceAttributes
in getattr(obj
, '__xmlattrnamespaces__').items():
472 if nameSpaceKey
== nameSpacePrefix
[:-1]: # Don't need to specify attribute namespace if it is the same as it selement
474 if attr
in nameSpaceAttributes
:
475 attrNameSpacePrefix
= nameSpaceKey
+ ':'
477 ## if attr.startswith('_'):
479 if (hasattr(obj
, "__xmlrename__") and attr
in obj
.__xmlrename
__):
480 attr
= obj
.__xmlrename
__[attr
]
482 objattrs
+= ' %s%s="%s"' % (attrNameSpacePrefix
, attr
, value
)
485 if isinstance(obj
, NoneType
):
487 # return '%s<%s objtype="None"/>%s' % (prefix, elementName, newline)
488 elif isinstance(obj
, bool):
489 return '%s<%s objtype="bool">%s</%s>%s' % (prefix
, elementName
, obj
, elementName
, newline
)
490 elif isinstance(obj
, int):
491 return '''%s<%s objtype="int">%s</%s>%s''' % (prefix
, elementName
, str(obj
), elementName
, newline
)
492 elif isinstance(obj
, long):
493 return '%s<%s objtype="long">%s</%s>%s' % (prefix
, elementName
, str(obj
), elementName
, newline
)
494 elif isinstance(obj
, float):
495 return '%s<%s objtype="float">%s</%s>%s' % (prefix
, elementName
, str(obj
), elementName
, newline
)
496 elif isinstance(obj
, basestring
):
497 return '''%s<%s>%s</%s>%s''' % (prefix
, elementName
, saxutils
.escape(obj
), elementName
, newline
)
498 ## elif isinstance(obj, unicode):
499 ## return '''%s<%s>%s</%s>%s''' % (prefix, elementName, obj, elementName, newline)
500 elif isinstance(obj
, list):
503 xmlString
= '%s<%s objtype="list">%s' % (prefix
, elementName
, newline
)
505 xmlString
+= marshal(item
, nameSpaces
=nameSpaces
, indent
=indent
+increment
)
506 xmlString
+= '%s</%s>%s' % (prefix
, elementName
, newline
)
508 elif isinstance(obj
, tuple):
511 xmlString
= '%s<%s objtype="list" mutable="false">%s' % (prefix
, elementName
, newline
)
513 xmlString
+= marshal(item
, nameSpaces
=nameSpaces
, indent
=indent
+increment
)
514 xmlString
+= '%s</%s>%s' % (prefix
, elementName
, newline
)
516 elif isinstance(obj
, dict):
517 xmlString
= '%s<%s objtype="dict">%s' % (prefix
, elementName
, newline
)
518 subprefix
= prefix
+ ' '*increment
519 subindent
= indent
+ 2*increment
520 for key
, val
in obj
.iteritems():
521 xmlString
+= "%s<key>%s%s%s</key>%s%s<value>%s%s%s</value>%s" \
522 % (subprefix
, newline
, marshal(key
, indent
=subindent
), subprefix
, newline
, subprefix
, newline
, marshal(val
, nameSpaces
=nameSpaces
, indent
=subindent
), subprefix
, newline
)
523 xmlString
+= '%s</%s>%s' % (prefix
, elementName
, newline
)
526 moduleName
= obj
.__class
__.__module
__
527 if (moduleName
== "activegrid.model.schema"):
528 xmlString
= '%s<%s%s%s' % (prefix
, elementName
, nameSpaceAttrs
, objattrs
)
530 xmlString
= '%s<%s%s%s objtype="%s.%s"' % (prefix
, elementName
, nameSpaceAttrs
, objattrs
, moduleName
, className
)
531 # get the member, value pairs for the object, filtering out
532 # the types we don't support.
534 if hasattr(obj
, '__xmlbody__'):
535 xmlMemberString
= getattr(obj
, obj
.__xmlbody
__)
537 entryList
= obj
.__dict
__.items()
538 ## # Add in properties
539 ## for key in obj.__class__.__dict__.iterkeys():
540 ## if (key not in members_to_skip and key not in obj.__dict__
541 ## and hasPropertyValue(obj, key)):
542 ## value = getattr(obj, key)
543 ## entryList.append((key, value))
545 for name
, value
in entryList
:
546 ## # special name handling for private "__*" attributes:
547 ## # remove the _<class-name> added by Python
548 ## if name.startswith(classNamePrefix): name = name[len(classNamePrefix):]
549 if name
in members_to_skip
: continue
550 if name
.startswith('__') and name
.endswith('__'): continue
551 ## idx = name.find('__')
553 ## newName = name[idx+2:]
556 subElementNameSpacePrefix
= nameSpacePrefix
557 if hasattr(obj
, '__xmlattrnamespaces__'):
558 for nameSpaceKey
, nameSpaceValues
in getattr(obj
, '__xmlattrnamespaces__').items():
559 if name
in nameSpaceValues
:
560 subElementNameSpacePrefix
= nameSpaceKey
+ ':'
562 # handle sequences listed in __xmlflattensequence__
563 # specially: instead of listing the contained items inside
564 # of a separate list, as god intended, list them inside
565 # the object containing the sequence.
566 if hasattr(obj
, '__xmlflattensequence__') and name
in obj
.__xmlflattensequence
__ and value
:
568 xmlnametuple
= obj
.__xmlflattensequence
__[name
]
570 if len(xmlnametuple
) == 1:
571 xmlname
= xmlnametuple
[0]
574 ## xmlname = name.lower()
575 for seqitem
in value
:
576 xmlMemberString
+= marshal(seqitem
, xmlname
, subElementNameSpacePrefix
, nameSpaces
=nameSpaces
, indent
=indent
+increment
)
578 if (hasattr(obj
, "__xmlrename__") and name
in obj
.__xmlrename
__):
579 xmlname
= obj
.__xmlrename
__[name
]
582 ## xmlname = name.lower()
584 ## if xmlname.startswith('_') and not xmlname.startswith('__'):
585 ## xmlname = xmlname[1:]
587 ## print "getting pretty deep, xmlname = ", xmlname
588 xmlMemberString
+= marshal(value
, xmlname
, subElementNameSpacePrefix
, nameSpaces
=nameSpaces
, indent
=indent
+increment
)
589 # if we have nested elements, add them here, otherwise close the element tag immediately.
592 if hasattr(obj
, '__xmlbody__'):
593 xmlString
+= xmlMemberString
594 xmlString
+= '</%s>%s' % (elementName
, newline
)
597 xmlString
+= xmlMemberString
598 xmlString
+= '%s</%s>%s' % (prefix
, elementName
, newline
)
600 xmlString
= xmlString
+ '/>%s' % newline
603 # We don't use this anymore but in case we want to get properties this is how
605 def hasPropertyValue(obj
, attr
):
608 prop
= obj
.__class
__.__dict
__[attr
]
609 if (isinstance(prop
, property)):
610 hasProp
= hasattr(obj
, attr
)
612 # It's a property and it has a value but sometimes we don't want it.
613 # If there is a _hasattr method execute it and the
614 # result will tell us whether to include this value
616 hasProp
= obj
._hasattr
(attr
)
623 if __name__
== '__main__':
624 from xmlmarshallertests
import Person
, marshalledint
, marshalledlist
629 xmlstr
= marshal(d
, "d", prettyPrint
=True)
633 person
.firstName
= "Albert"
634 person
.lastName
= "Camus"
635 person
.addressLine1
= "23 Absurd St."
636 person
.city
= "Ennui"
639 person
._phoneNumber
= "808-303-2323"
640 person
.favoriteWords
= ['angst', 'ennui', 'existence']
643 xmlstring
= marshal(person
, 'person', prettyPrint
=True)
646 obj
= unmarshal(marshalledlist
)
647 print "obj has type %s and value %s" % (type(obj
), str(obj
))
649 print "item: %s" % str(item
)