dexml-0.5.1/0000755000175000017500000000000012042330626012212 5ustar rfkrfk00000000000000dexml-0.5.1/setup.py0000644000175000017500000000445112042330443013725 0ustar rfkrfk00000000000000# # This is the dexml setuptools script. # Originally developed by Ryan Kelly, 2009. # # This script is placed in the public domain. # import sys setup_kwds = {} # Use setuptools is available, so we have `python setup.py test`. # We also need it for 2to3 integration on python3. # Otherwise, fall back to plain old distutils. try: from setuptools import setup except ImportError: if sys.version_info > (3,): raise RuntimeError("python3 support requires setuptools") from distutils.core import setup else: setup_kwds["test_suite"] = "dexml.test" if sys.version_info > (3,): setup_kwds["use_2to3"] = True # Extract the docstring and version declaration from the module. # To avoid errors due to missing dependencies or bad python versions, # we explicitly read the file contents up to the end of the version # delcaration, then exec it ourselves. info = {} src = open("dexml/__init__.py") lines = [] for ln in src: lines.append(ln) if "__version__" in ln: for ln in src: if "__version__" not in ln: break lines.append(ln) break exec("".join(lines),info) NAME = "dexml" VERSION = info["__version__"] DESCRIPTION = "a dead-simple Object-XML mapper for Python" LONG_DESC = info["__doc__"] AUTHOR = "Ryan Kelly" AUTHOR_EMAIL = "ryan@rfk.id.au" URL="http://packages.python.org/dexml" LICENSE = "MIT" KEYWORDS = "xml" CLASSIFIERS = [ "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "License :: OSI Approved", "License :: OSI Approved :: MIT License", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Text Processing", "Topic :: Text Processing :: Markup", "Topic :: Text Processing :: Markup :: XML", ] setup(name=NAME, version=VERSION, author=AUTHOR, author_email=AUTHOR_EMAIL, url=URL, description=DESCRIPTION, long_description=LONG_DESC, license=LICENSE, keywords=KEYWORDS, packages=["dexml"], classifiers=CLASSIFIERS, **setup_kwds ) dexml-0.5.1/MANIFEST.in0000644000175000017500000000007711723311557013763 0ustar rfkrfk00000000000000 include README.rst include LICENSE.txt include ChangeLog.txt dexml-0.5.1/PKG-INFO0000644000175000017500000000731212042330626013312 0ustar rfkrfk00000000000000Metadata-Version: 1.1 Name: dexml Version: 0.5.1 Summary: a dead-simple Object-XML mapper for Python Home-page: http://packages.python.org/dexml Author: Ryan Kelly Author-email: ryan@rfk.id.au License: MIT Description: dexml: a dead-simple Object-XML mapper for Python ================================================== Let's face it: xml is a fact of modern life. I'd even go so far as to say that it's *good* at what is does. But that doesn't mean it's easy to work with and it doesn't mean that we have to like it. Most of the time, XML just needs to get out of the way and let you do some actual work instead of writing code to traverse and manipulate yet another DOM. The dexml module takes the obvious mapping between XML tags and Python objects and lets you capture that as cleanly as possible. Loosely inspired by Django's ORM, you write simple class definitions to define the expected structure of your XML document. Like so:: >>> import dexml >>> from dexml import fields >>> class Person(dexml.Model): ... name = fields.String() ... age = fields.Integer(tagname='age') Then you can parse an XML document into an object like this:: >>> p = Person.parse("42") >>> p.name u'Foo McBar' >>> p.age 42 And you can render an object into an XML document like this:: >>> p = Person(name="Handsome B. Wonderful",age=36) >>> p.render() '36' Malformed documents will raise a ParseError:: >>> p = Person.parse("92") Traceback (most recent call last): ... ParseError: required field not found: 'name' Of course, it gets more interesting when you nest Model definitions, like this:: >>> class Group(dexml.Model): ... name = fields.String(attrname="name") ... members = fields.List(Person) ... >>> g = Group(name="Monty Python") >>> g.members.append(Person(name="John Cleese",age=69)) >>> g.members.append(Person(name="Terry Jones",age=67)) >>> g.render(fragment=True) '6967' There's support for XML namespaces, default field values, case-insensitive parsing, and more fun stuff. Check out the documentation on the following classes for more details: :Model: the base class for objects that map into XML :Field: the base class for individual model fields :Meta: meta-information about how to parse/render a model Keywords: xml Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: License :: OSI Approved Classifier: License :: OSI Approved :: MIT License Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Topic :: Text Processing Classifier: Topic :: Text Processing :: Markup Classifier: Topic :: Text Processing :: Markup :: XML dexml-0.5.1/dexml.egg-info/0000755000175000017500000000000012042330626015015 5ustar rfkrfk00000000000000dexml-0.5.1/dexml.egg-info/SOURCES.txt0000644000175000017500000000033512042330623016677 0ustar rfkrfk00000000000000ChangeLog.txt LICENSE.txt MANIFEST.in README.rst setup.py dexml/__init__.py dexml/fields.py dexml/test.py dexml.egg-info/PKG-INFO dexml.egg-info/SOURCES.txt dexml.egg-info/dependency_links.txt dexml.egg-info/top_level.txtdexml-0.5.1/dexml.egg-info/PKG-INFO0000644000175000017500000000731212042330623016112 0ustar rfkrfk00000000000000Metadata-Version: 1.1 Name: dexml Version: 0.5.1 Summary: a dead-simple Object-XML mapper for Python Home-page: http://packages.python.org/dexml Author: Ryan Kelly Author-email: ryan@rfk.id.au License: MIT Description: dexml: a dead-simple Object-XML mapper for Python ================================================== Let's face it: xml is a fact of modern life. I'd even go so far as to say that it's *good* at what is does. But that doesn't mean it's easy to work with and it doesn't mean that we have to like it. Most of the time, XML just needs to get out of the way and let you do some actual work instead of writing code to traverse and manipulate yet another DOM. The dexml module takes the obvious mapping between XML tags and Python objects and lets you capture that as cleanly as possible. Loosely inspired by Django's ORM, you write simple class definitions to define the expected structure of your XML document. Like so:: >>> import dexml >>> from dexml import fields >>> class Person(dexml.Model): ... name = fields.String() ... age = fields.Integer(tagname='age') Then you can parse an XML document into an object like this:: >>> p = Person.parse("42") >>> p.name u'Foo McBar' >>> p.age 42 And you can render an object into an XML document like this:: >>> p = Person(name="Handsome B. Wonderful",age=36) >>> p.render() '36' Malformed documents will raise a ParseError:: >>> p = Person.parse("92") Traceback (most recent call last): ... ParseError: required field not found: 'name' Of course, it gets more interesting when you nest Model definitions, like this:: >>> class Group(dexml.Model): ... name = fields.String(attrname="name") ... members = fields.List(Person) ... >>> g = Group(name="Monty Python") >>> g.members.append(Person(name="John Cleese",age=69)) >>> g.members.append(Person(name="Terry Jones",age=67)) >>> g.render(fragment=True) '6967' There's support for XML namespaces, default field values, case-insensitive parsing, and more fun stuff. Check out the documentation on the following classes for more details: :Model: the base class for objects that map into XML :Field: the base class for individual model fields :Meta: meta-information about how to parse/render a model Keywords: xml Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: License :: OSI Approved Classifier: License :: OSI Approved :: MIT License Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Topic :: Text Processing Classifier: Topic :: Text Processing :: Markup Classifier: Topic :: Text Processing :: Markup :: XML dexml-0.5.1/dexml.egg-info/top_level.txt0000644000175000017500000000000612042330623017540 0ustar rfkrfk00000000000000dexml dexml-0.5.1/dexml.egg-info/dependency_links.txt0000644000175000017500000000000112042330623021060 0ustar rfkrfk00000000000000 dexml-0.5.1/dexml/0000755000175000017500000000000012042330626013323 5ustar rfkrfk00000000000000dexml-0.5.1/dexml/test.py0000644000175000017500000012730312042330375014663 0ustar rfkrfk00000000000000""" dexml.test: testcases for dexml module. """ import sys import os import os.path import difflib import unittest import doctest from xml.dom import minidom from StringIO import StringIO import dexml from dexml import fields def b(raw): """Compatability wrapper for b"string" syntax.""" return raw.encode("ascii") def model_fields_equal(m1,m2): """Check for equality by comparing model fields.""" for nm in m1.__class__._fields: v1 = getattr(m1,nm.field_name) v2 = getattr(m2,nm.field_name) if isinstance(v1,dexml.Model): if not model_fields_equal(v1,v2): return False elif v1 != v2: return False return True class TestDexmlDocstring(unittest.TestCase): def test_docstring(self): """Test dexml docstrings We don't do this on python3 because of the many small ways in which the output has changed in that version. """ if sys.version_info < (3,): assert doctest.testmod(dexml)[0] == 0 def test_readme_matches_docstring(self): """Ensure that the README is in sync with the docstring. This test should always pass; if the README is out of sync it just updates it with the contents of dexml.__doc__. """ dirname = os.path.dirname readme = os.path.join(dirname(dirname(__file__)),"README.rst") if not os.path.isfile(readme): f = open(readme,"wb") f.write(dexml.__doc__.encode()) f.close() else: f = open(readme,"rb") if f.read() != dexml.__doc__: f.close() f = open(readme,"wb") f.write(dexml.__doc__.encode()) f.close() class TestDexml(unittest.TestCase): def test_base(self): """Test operation of a dexml.Model class with no fields.""" class hello(dexml.Model): pass h = hello.parse("") self.assertTrue(h) h = hello.parse("\n") self.assertTrue(h) h = hello.parse("world") self.assertTrue(h) d = minidom.parseString("world") h = hello.parse(d) self.assertTrue(h) self.assertRaises(dexml.ParseError,hello.parse,"") self.assertRaises(dexml.ParseError,hello.parse,"") self.assertRaises(dexml.ParseError,hello.parse,"") hello.meta.ignore_unknown_elements = False self.assertRaises(dexml.ParseError,hello.parse,"world") hello.meta.ignore_unknown_elements = True h = hello() self.assertEquals(h.render(),'') self.assertEquals(h.render(fragment=True),"") self.assertEquals(h.render(encoding="utf8"),b('')) self.assertEquals(h.render(encoding="utf8",fragment=True),b("")) self.assertEquals(h.render(),"".join(h.irender())) self.assertEquals(h.render(fragment=True),"".join(h.irender(fragment=True))) self.assertEquals(h.render(encoding="utf8"),b("").join(h.irender(encoding="utf8"))) self.assertEquals(h.render(encoding="utf8",fragment=True),b("").join(h.irender(encoding="utf8",fragment=True))) def test_errors_on_malformed_xml(self): class hello(dexml.Model): pass self.assertRaises(dexml.XmlError,hello.parse,b("")) self.assertRaises(dexml.XmlError,hello.parse,b("")) self.assertRaises(dexml.XmlError,hello.parse,b("")) self.assertRaises(dexml.XmlError,hello.parse,u"") self.assertRaises(dexml.XmlError,hello.parse,u"") self.assertRaises(dexml.XmlError,hello.parse,u"") self.assertRaises(dexml.XmlError,hello.parse,StringIO("")) self.assertRaises(dexml.XmlError,hello.parse,StringIO("")) self.assertRaises(dexml.XmlError,hello.parse,StringIO("")) self.assertRaises(ValueError,hello.parse,None) self.assertRaises(ValueError,hello.parse,42) self.assertRaises(ValueError,hello.parse,staticmethod) def test_unicode_model_tagname(self): """Test a dexml.Model class with a unicode tag name.""" class hello(dexml.Model): class meta: tagname = u"hel\N{GREEK SMALL LETTER LAMDA}o" h = hello.parse(u"") self.assertTrue(h) h = hello.parse(u"\n") self.assertTrue(h) self.assertRaises(dexml.ParseError,hello.parse,u"") self.assertRaises(dexml.ParseError,hello.parse,u"") self.assertRaises(dexml.ParseError,hello.parse,u"") self.assertRaises(dexml.ParseError,hello.parse,u"") h = hello.parse(u"world") self.assertTrue(h) h = hello.parse(u"world") h = hello.parse(u"world") self.assertTrue(h) h = hello() self.assertEquals(h.render(),u'') self.assertEquals(h.render(fragment=True),u"") self.assertEquals(h.render(encoding="utf8"),u''.encode("utf8")) self.assertEquals(h.render(encoding="utf8",fragment=True),u"".encode("utf8")) self.assertEquals(h.render(),"".join(h.irender())) self.assertEquals(h.render(fragment=True),"".join(h.irender(fragment=True))) self.assertEquals(h.render(encoding="utf8"),b("").join(h.irender(encoding="utf8"))) self.assertEquals(h.render(encoding="utf8",fragment=True),b("").join(h.irender(encoding="utf8",fragment=True))) def test_unicode_string_field(self): """Test a dexml.Model class with a unicode string field.""" class Person(dexml.Model): name = fields.String() p = Person.parse(u"") self.assertEquals(p.name, u"hel\N{GREEK SMALL LETTER LAMDA}o") p = Person() p.name = u"hel\N{GREEK SMALL LETTER LAMDA}o" self.assertEquals(p.render(encoding="utf8"), u''.encode("utf8")) def test_model_meta_attributes(self): class hello(dexml.Model): pass self.assertRaises(dexml.ParseError,hello.parse,"") hello.meta.case_sensitive = False self.assertTrue(hello.parse("")) self.assertRaises(dexml.ParseError,hello.parse,"") hello.meta.case_sensitive = True self.assertTrue(hello.parse("world")) hello.meta.ignore_unknown_elements = False self.assertRaises(dexml.ParseError,hello.parse,"world") hello.meta.ignore_unknown_elements = True def test_namespace(self): """Test basic handling of namespaces.""" class hello(dexml.Model): class meta: namespace = "http://hello.com/" ignore_unknown_elements = False h = hello.parse("") self.assertTrue(h) h = hello.parse("") self.assertTrue(h) self.assertRaises(dexml.ParseError,hello.parse,"") self.assertRaises(dexml.ParseError,hello.parse,"") self.assertRaises(dexml.ParseError,hello.parse,"world") hello.meta.case_sensitive = False self.assertRaises(dexml.ParseError,hello.parse,"") self.assertRaises(dexml.ParseError,hello.parse,"") self.assertRaises(dexml.ParseError,hello.parse,"") hello.parse("") hello.meta.case_sensitive = True h = hello() self.assertEquals(h.render(fragment=True),'') hello.meta.namespace_prefix = "H" self.assertEquals(h.render(fragment=True),'') def test_base_field(self): """Test operation of the base Field class (for coverage purposes).""" class tester(dexml.Model): value = fields.Field() assert isinstance(tester.value,fields.Field) # This is a parse error because Field doesn't consume any nodes self.assertRaises(dexml.ParseError,tester.parse,"") self.assertRaises(dexml.ParseError,tester.parse,"42") # Likewise, Field doesn't output any XML so it thinks value is missing self.assertRaises(dexml.RenderError,tester(value=None).render) def test_value_fields(self): """Test operation of basic value fields.""" class hello(dexml.Model): recipient = fields.String() sentby = fields.String(attrname="sender") strength = fields.Integer(default=1) message = fields.String(tagname="msg") h = hello.parse("hi there") self.assertEquals(h.recipient,"ryan") self.assertEquals(h.sentby,"lozz") self.assertEquals(h.message,"hi there") self.assertEquals(h.strength,7) # These are parse errors due to namespace mismatches self.assertRaises(dexml.ParseError,hello.parse,"hi there") self.assertRaises(dexml.ParseError,hello.parse,"hi there") # These are parse errors due to subtags self.assertRaises(dexml.ParseError,hello.parse,"hi there") def test_float_field(self): class F(dexml.Model): value = fields.Float() self.assertEquals(F.parse("").value,4.2) def test_boolean_field(self): class F(dexml.Model): value = fields.Boolean() self.assertTrue(F.parse("").value) self.assertTrue(F.parse("").value) self.assertTrue(F.parse("").value) self.assertFalse(F.parse("").value) self.assertFalse(F.parse("").value) self.assertFalse(F.parse("").value) f = F.parse("") assert model_fields_equal(F.parse(f.render()),f) f.value = "someotherthing" assert model_fields_equal(F.parse(f.render()),f) f.value = False assert model_fields_equal(F.parse(f.render()),f) def test_string_with_special_chars(self): class letter(dexml.Model): message = fields.String(tagname="msg") l = letter.parse("hello & goodbye") self.assertEquals(l.message,"hello & goodbye") l = letter.parse("") self.assertEquals(l.message,"hello & goodbye") l = letter(message="XML are fun!") self.assertEquals(l.render(fragment=True),'XML <tags> are fun!') class update(dexml.Model): status = fields.String(attrname="status") u = update(status="feeling !") self.assertEquals(u.render(fragment=True),'') def test_cdata_fields(self): try: class update(dexml.Model): status = fields.CDATA() assert False, "CDATA allowed itself to be created without tagname" except ValueError: pass class update(dexml.Model): status = fields.CDATA(tagname=True) u = update(status="feeling !") self.assertEquals(u.render(fragment=True),'!]]>') def test_model_field(self): """Test operation of fields.Model.""" class person(dexml.Model): name = fields.String() age = fields.Integer() class pet(dexml.Model): name = fields.String() species = fields.String(required=False) class Vet(dexml.Model): class meta: tagname = "vet" name = fields.String() class pets(dexml.Model): person = fields.Model() pet1 = fields.Model("pet") pet2 = fields.Model(pet,required=False) pet3 = fields.Model((None,pet),required=False) vet = fields.Model((None,"Vet"),required=False) p = pets.parse("") self.assertEquals(p.person.name,"ryan") self.assertEquals(p.pet1.species,"dog") self.assertEquals(p.pet2,None) p = pets.parse("\n\n\n\n") self.assertEquals(p.person.name,"ryan") self.assertEquals(p.pet1.name,"riley") self.assertEquals(p.pet2.species,"fish") p = pets.parse("") self.assertEquals(p.person.name,"ryan") self.assertEquals(p.pet1.name,"riley") self.assertEquals(p.pet2.species,"fish") self.assertEquals(p.pet3.species,"cat") self.assertEquals(p.vet.name,"Nic") self.assertRaises(dexml.ParseError,pets.parse,"") self.assertRaises(dexml.ParseError,pets.parse,"") def assign(val): p.pet1 = val self.assertRaises(ValueError, assign, person(name = 'ryan', age = 26)) self.assertEquals(p.pet1.name,"riley") assign(pet(name="spike")) self.assertEquals(p.pet1.name,"spike") p = pets() self.assertRaises(dexml.RenderError,p.render) p.person = person(name="lozz",age="25") p.pet1 = pet(name="riley") self.assertEquals(p.render(fragment=True),'') self.assertEquals("".join(p.irender(fragment=True)),'') p.pet2 = pet(name="guppy",species="fish") self.assertEquals(p.render(fragment=True),'') self.assertEquals("".join(p.irender(fragment=True)),'') def test_model_field_namespace(self): """Test operation of fields.Model with namespaces""" class petbase(dexml.Model): class meta: namespace = "http://www.pets.com/PetML" namespace_prefix = "P" class person(petbase): name = fields.String() age = fields.Integer() status = fields.String(tagname=("S:","status"),required=False) class pet(petbase): name = fields.String() species = fields.String(required=False) class pets(petbase): person = fields.Model() pet1 = fields.Model("pet") pet2 = fields.Model(pet,required=False) p = pets.parse("") self.assertEquals(p.person.name,"ryan") self.assertEquals(p.pet1.species,"dog") self.assertEquals(p.pet2,None) p = pets.parse("") self.assertEquals(p.person.name,"ryan") self.assertEquals(p.pet1.name,"riley") self.assertEquals(p.pet2.species,"fish") self.assertRaises(dexml.ParseError,pets.parse,"") self.assertRaises(dexml.ParseError,pets.parse,"") p = pets() self.assertRaises(dexml.RenderError,p.render) p.person = person(name="lozz",age="25") p.pet1 = pet(name="riley") self.assertEquals(p.render(fragment=True),'') p.pet2 = pet(name="guppy",species="fish") self.assertEquals(p.render(fragment=True),'') p = person.parse('awesome') self.assertEquals(p.status,None) p = person.parse('awesome') self.assertEquals(p.status,None) p = person.parse('awesome') self.assertEquals(p.status,None) p = person.parse('awesome') self.assertEquals(p.status,"awesome") def test_list_field(self): """Test operation of fields.List""" class person(dexml.Model): name = fields.String() age = fields.Integer() class pet(dexml.Model): name = fields.String() species = fields.String(required=False) class reward(dexml.Model): date = fields.String() class pets(dexml.Model): person = fields.Model() pets = fields.List("pet",minlength=1) notes = fields.List(fields.String(tagname="note"),maxlength=2) rewards = fields.List("reward",tagname="rewards",required=False) p = pets.parse("") self.assertEquals(p.person.name,"ryan") self.assertEquals(p.pets[0].species,"dog") self.assertEquals(len(p.pets),1) self.assertEquals(len(p.notes),0) p = pets.parse("\n\t\n\t\n\t\n\tnoted") self.assertEquals(p.person.name,"ryan") self.assertEquals(p.pets[0].name,"riley") self.assertEquals(p.pets[1].species,"fish") self.assertEquals(p.notes[0],"noted") self.assertEquals(len(p.pets),2) self.assertEquals(len(p.notes),1) self.assertRaises(dexml.ParseError,pets.parse,"") self.assertRaises(dexml.ParseError,pets.parse,"") self.assertRaises(dexml.ParseError,pets.parse,"toomanynotes") p = pets() p.person = person(name="lozz",age="25") self.assertRaises(dexml.RenderError,p.render) p.pets.append(pet(name="riley")) self.assertEquals(p.render(fragment=True),'') p.pets.append(pet(name="guppy",species="fish")) p.notes.append("noted") self.assertEquals(p.render(fragment=True),'noted') p = pets() p.person = person(name="lozz",age="25") yielded_items = [] def gen_pets(): for p in (pet(name="riley"),pet(name="guppy",species="fish")): yielded_items.append(p) yield p p.pets = gen_pets() self.assertEquals(len(yielded_items),0) p.notes.append("noted") self.assertEquals(p.render(fragment=True),'noted') self.assertEquals(len(yielded_items),2) p = pets.parse("") self.assertEquals(len(p.rewards), 2) self.assertEquals(p.rewards[1].date, 'November 10, 2009') self.assertEquals(p.render(fragment = True), '') pets.meta.ignore_unknown_elements = False self.assertRaises(dexml.ParseError, pets.parse, "") def test_list_field_tagname(self): """Test List(tagname="items",required=True).""" class obj(dexml.Model): items = fields.List(fields.String(tagname="item"),tagname="items") o = obj(items=[]) self.assertEquals(o.render(fragment=True), '') self.assertRaises(dexml.ParseError,obj.parse,'') o = obj.parse('') self.assertEquals(o.items,[]) def test_list_field_sanity_checks(self): class GreedyField(fields.Field): def parse_child_node(self,obj,node): return dexml.PARSE_MORE class SaneList(dexml.Model): item = fields.List(GreedyField(tagname="item")) self.assertRaises(ValueError,SaneList.parse,"") def test_list_field_max_min(self): try: class MyStuff(dexml.Model): items = fields.List(fields.String(tagname="item"),required=False,minlength=2) assert False, "List allowed creation with nonsensical args" except ValueError: pass class MyStuff(dexml.Model): items = fields.List(fields.String(tagname="item"),required=False) self.assertEquals(MyStuff.parse("").items,[]) MyStuff.items.maxlength = 1 self.assertEquals(MyStuff.parse("").items,[""]) self.assertRaises(dexml.ParseError,MyStuff.parse,"") s = MyStuff() s.items = ["one","two"] self.assertRaises(dexml.RenderError,s.render) MyStuff.items.maxlength = None MyStuff.items.minlength = 2 MyStuff.items.required = True self.assertEquals(MyStuff.parse("").items,["",""]) self.assertRaises(dexml.ParseError,MyStuff.parse,"") def test_dict_field(self): """Test operation of fields.Dict""" class item(dexml.Model): name = fields.String() attr = fields.String(tagname = 'attr') class obj(dexml.Model): items = fields.Dict('item', key = 'name') xml = 'val1val2' o = obj.parse(xml) self.assertEquals(len(o.items), 2) self.assertEquals(o.items['item1'].name, 'item1') self.assertEquals(o.items['item2'].attr, 'val2') del o.items['item2'] self.assertEquals(o.render(fragment = True), 'val1') o.items['item3'] = item(attr = 'val3') self.assertEquals(o.items['item3'].attr, 'val3') def _setitem(): o.items['item3'] = item(name = 'item2', attr = 'val3') self.assertRaises(ValueError, _setitem) class obj(dexml.Model): items = fields.Dict(fields.Model(item), key = 'name', unique = True) xml = 'val1val2' self.assertRaises(dexml.ParseError, obj.parse, xml) class obj(dexml.Model): items = fields.Dict('item', key = 'name', tagname = 'items') xml = ' val1 val2 ' o = obj.parse(xml) self.assertEquals(len(o.items), 2) self.assertEquals(o.items['item1'].name, 'item1') self.assertEquals(o.items['item2'].attr, 'val2') del o.items['item2'] self.assertEquals(o.render(fragment = True), 'val1') # Test that wrapper tags are still required even for empty fields o = obj(items={}) self.assertEquals(o.render(fragment=True), '') o = obj.parse('') self.assertEquals(o.items,{}) self.assertRaises(dexml.ParseError,obj.parse,'') obj.items.required = False self.assertEquals(o.render(fragment=True), '') obj.items.required = True from collections import defaultdict class _dict(defaultdict): def __init__(self): super(_dict, self).__init__(item) class obj(dexml.Model): items = fields.Dict('item', key = 'name', dictclass = _dict) o = obj() self.assertEquals(o.items['item1'].name, 'item1') def test_dict_field_sanity_checks(self): class GreedyField(fields.Field): def parse_child_node(self,obj,node): return dexml.PARSE_MORE class SaneDict(dexml.Model): item = fields.Dict(GreedyField(tagname="item"),key="name") self.assertRaises(ValueError,SaneDict.parse,"") class item(dexml.Model): name = fields.String() value = fields.String() class MyStuff(dexml.Model): items = fields.Dict(item,key="wrongname") self.assertRaises(dexml.ParseError,MyStuff.parse,"") def test_dict_field_max_min(self): class item(dexml.Model): name = fields.String() value = fields.String() try: class MyStuff(dexml.Model): items = fields.Dict(item,key="name",required=False,minlength=2) assert False, "Dict allowed creation with nonsensical args" except ValueError: pass class MyStuff(dexml.Model): items = fields.Dict(item,key="name",required=False) self.assertEquals(MyStuff.parse("").items,{}) MyStuff.items.maxlength = 1 self.assertEquals(len(MyStuff.parse("").items),1) self.assertRaises(dexml.ParseError,MyStuff.parse,"") s = MyStuff() s.items = [item(name="yo",value="dawg"),item(name="wazzup",value="yo")] self.assertRaises(dexml.RenderError,s.render) MyStuff.items.maxlength = None MyStuff.items.minlength = 2 MyStuff.items.required = True self.assertEquals(len(MyStuff.parse("").items),2) self.assertRaises(dexml.ParseError,MyStuff.parse,"") s = MyStuff() s.items = [item(name="yo",value="dawg")] self.assertRaises(dexml.RenderError,s.render) def test_choice_field(self): """Test operation of fields.Choice""" class breakfast(dexml.Model): meal = fields.Choice("bacon","cereal") class bacon(dexml.Model): num_rashers = fields.Integer() class cereal(dexml.Model): with_milk = fields.Boolean() b = breakfast.parse("") self.assertEquals(b.meal.num_rashers,4) b = breakfast.parse("") self.assertTrue(b.meal.with_milk) self.assertRaises(dexml.ParseError,b.parse,"") self.assertRaises(dexml.ParseError,b.parse,"") b = breakfast() self.assertRaises(dexml.RenderError,b.render) b.meal = bacon(num_rashers=1) self.assertEquals(b.render(fragment=True),"") def test_choice_field_sanity_checks(self): try: class SaneChoice(dexml.Model): item = fields.Choice(fields.String(),fields.Integer()) assert False, "Choice field failed its sanity checks" except ValueError: pass class GreedyModel(fields.Model): def parse_child_node(self,obj,node): return dexml.PARSE_MORE class SaneChoice(dexml.Model): item = fields.Choice(GreedyModel("SaneChoice")) self.assertRaises(ValueError,SaneChoice.parse,"") def test_list_of_choice(self): """Test operation of fields.Choice inside fields.List""" class breakfast(dexml.Model): meals = fields.List(fields.Choice("bacon","cereal")) class bacon(dexml.Model): num_rashers = fields.Integer() class cereal(dexml.Model): with_milk = fields.Boolean() b = breakfast.parse("") self.assertEquals(len(b.meals),1) self.assertEquals(b.meals[0].num_rashers,4) b = breakfast.parse("") self.assertEquals(len(b.meals),2) self.assertEquals(b.meals[0].num_rashers,2) self.assertTrue(b.meals[1].with_milk) def test_empty_only_boolean(self): """Test operation of fields.Boolean with empty_only=True""" class toggles(dexml.Model): toggle_str = fields.Boolean(required=False) toggle_empty = fields.Boolean(tagname=True,empty_only=True) t = toggles.parse("") self.assertFalse(t.toggle_str) self.assertFalse(t.toggle_empty) t = toggles.parse("") self.assertTrue(t.toggle_str) self.assertTrue(t.toggle_empty) t = toggles.parse("") self.assertFalse(t.toggle_str) self.assertTrue(t.toggle_empty) self.assertRaises(ValueError,toggles.parse,"no") self.assertFalse("toggle_empty" in toggles(toggle_empty=False).render()) self.assertTrue("" in toggles(toggle_empty=True).render()) def test_XmlNode(self): """Test correct operation of fields.XmlNode.""" class bucket(dexml.Model): class meta: namespace = "bucket-uri" contents = fields.XmlNode(encoding="utf8") b = bucket.parse("") self.assertEquals(b.contents.childNodes[0].tagName,"hello") self.assertEquals(b.contents.childNodes[0].namespaceURI,None) self.assertEquals(b.contents.childNodes[0].childNodes[0].localName,"world") self.assertEquals(b.contents.childNodes[0].childNodes[0].namespaceURI,"bucket-uri") b = bucket() b.contents = "world" b = bucket.parse(b.render()) self.assertEquals(b.contents.tagName,"hello") b.contents = u"world" b = bucket.parse(b.render()) self.assertEquals(b.contents.tagName,"hello") b = bucket.parse("") b2 = bucket.parse("".join(fields.XmlNode.render_children(b,b.contents,{}))) self.assertEquals(b2.contents.tagName,"hello") class bucket(dexml.Model): class meta: namespace = "bucket-uri" contents = fields.XmlNode(tagname="contents") b = bucket.parse("") self.assertEquals(b.contents.childNodes[0].tagName,"hello") def test_namespaced_attrs(self): class nsa(dexml.Model): f1 = fields.Integer(attrname=("test:","f1")) n = nsa.parse("") self.assertEquals(n.f1,7) n2 = nsa.parse(n.render()) self.assertEquals(n2.f1,7) class nsa_decl(dexml.Model): class meta: tagname = "nsa" namespace = "test:" namespace_prefix = "t" f1 = fields.Integer(attrname=("test:","f1")) n = nsa_decl.parse("") self.assertEquals(n.f1,7) self.assertEquals(n.render(fragment=True),'') def test_namespaced_children(self): class nsc(dexml.Model): f1 = fields.Integer(tagname=("test:","f1")) n = nsc.parse("7") self.assertEquals(n.f1,7) n2 = nsc.parse(n.render()) self.assertEquals(n2.f1,7) n = nsc.parse("7") self.assertEquals(n.f1,7) n2 = nsc.parse(n.render()) self.assertEquals(n2.f1,7) class nsc_decl(dexml.Model): class meta: tagname = "nsc" namespace = "test:" namespace_prefix = "t" f1 = fields.Integer(tagname=("test:","f1")) n = nsc_decl.parse("7") self.assertEquals(n.f1,7) n2 = nsc_decl.parse(n.render()) self.assertEquals(n2.f1,7) n = nsc_decl.parse("7") self.assertEquals(n.f1,7) n2 = nsc_decl.parse(n.render()) self.assertEquals(n2.f1,7) self.assertEquals(n2.render(fragment=True),'7') def test_order_sensitive(self): """Test operation of order-sensitive and order-insensitive parsing""" class junk(dexml.Model): class meta: order_sensitive = True name = fields.String(tagname=True) notes = fields.List(fields.String(tagname="note")) amount = fields.Integer(tagname=True) class junk_unordered(junk): class meta: tagname = "junk" order_sensitive = False j = junk.parse("test1note1note27") self.assertEquals(j.name,"test1") self.assertEquals(j.notes,["note1","note2"]) self.assertEquals(j.amount,7) j = junk_unordered.parse("test1note1note27") self.assertEquals(j.name,"test1") self.assertEquals(j.notes,["note1","note2"]) self.assertEquals(j.amount,7) self.assertRaises(dexml.ParseError,junk.parse,"note17note2test1") j = junk_unordered.parse("note17note2test1") self.assertEquals(j.name,"test1") self.assertEquals(j.notes,["note1","note2"]) self.assertEquals(j.amount,7) def test_namespace_prefix_generation(self): class A(dexml.Model): class meta: namespace='http://xxx' a = fields.String(tagname=('http://yyy','a')) class B(dexml.Model): class meta: namespace='http://yyy' b = fields.Model(A) b1 = B(b=A(a='value')) # With no specific prefixes set we can't predict the output, # but it should round-trip OK. assert model_fields_equal(B.parse(b1.render()),b1) # With specific prefixes set, output is predictable. A.meta.namespace_prefix = "x" B.meta.namespace_prefix = "y" self.assertEquals(b1.render(),'value') A.meta.namespace_prefix = None B.meta.namespace_prefix = None # This is a little hackery to trick the random-prefix generator # into looping a few times before picking one. We can't predict # the output but it'll exercise the code. class pickydict(dict): def __init__(self,*args,**kwds): self.__counter = 0 super(pickydict,self).__init__(*args,**kwds) def __contains__(self,key): if self.__counter > 5: return super(pickydict,self).__contains__(key) self.__counter += 1 return True assert model_fields_equal(B.parse(b1.render(nsmap=pickydict())),b1) class A(dexml.Model): class meta: namespace='T:' a = fields.String(attrname=('A:','a')) b = fields.String(attrname=(None,'b')) c = fields.String(tagname=(None,'c')) a1 = A(a="hello",b="world",c="owyagarn") # With no specific prefixes set we can't predict the output, # but it should round-trip OK. assert model_fields_equal(A.parse(a1.render()),a1) # With specific prefixes set, output is predictable. # Note that this suppresses generation of the xmlns declarations, # so the output is actually broken here. Broken, but predictable. nsmap = {} nsmap["T"] = ["T:"] nsmap["A"] = ["A:"] self.assertEquals(a1.render(fragment=True,nsmap=nsmap),'owyagarn') # This is a little hackery to trick the random-prefix generator # into looping a few times before picking one. We can't predict # the output but it'll exercise the code. class pickydict(dict): def __init__(self,*args,**kwds): self.__counter = 0 super(pickydict,self).__init__(*args,**kwds) def __contains__(self,key): if self.__counter > 5: return super(pickydict,self).__contains__(key) self.__counter += 1 return True assert model_fields_equal(A.parse(a1.render(nsmap=pickydict())),a1) A.c.tagname = ("C:","c") assert model_fields_equal(A.parse(a1.render(nsmap=pickydict())),a1) a1 = A(a="hello",b="world",c="") assert model_fields_equal(A.parse(a1.render(nsmap=pickydict())),a1) def test_parsing_value_from_tag_contents(self): class attr(dexml.Model): name = fields.String() value = fields.String(tagname=".") class obj(dexml.Model): id = fields.String() attrs = fields.List(attr) o = obj.parse('6description') self.assertEquals(o.id,"z108") self.assertEquals(len(o.attrs),2) self.assertEquals(o.attrs[0].name,"level") self.assertEquals(o.attrs[0].value,"6") self.assertEquals(o.attrs[1].name,"descr") self.assertEquals(o.attrs[1].value,"description") o = obj(id="test") o.attrs.append(attr(name="hello",value="world")) o.attrs.append(attr(name="wherethe",value="bloodyhellareya")) self.assertEquals(o.render(fragment=True),'worldbloodyhellareya') def test_inheritance_of_meta_attributes(self): class Base1(dexml.Model): class meta: tagname = "base1" order_sensitive = True class Base2(dexml.Model): class meta: tagname = "base2" order_sensitive = False class Sub(Base1): pass self.assertEquals(Sub.meta.order_sensitive,True) class Sub(Base2): pass self.assertEquals(Sub.meta.order_sensitive,False) class Sub(Base2): class meta: order_sensitive = True self.assertEquals(Sub.meta.order_sensitive,True) class Sub(Base1,Base2): pass self.assertEquals(Sub.meta.order_sensitive,True) class Sub(Base2,Base1): pass self.assertEquals(Sub.meta.order_sensitive,False) def test_mixing_in_other_base_classes(self): class Thing(dexml.Model): testit = fields.String() class Mixin(object): def _get_testit(self): return 42 def _set_testit(self,value): pass testit = property(_get_testit,_set_testit) class Sub(Thing,Mixin): pass assert issubclass(Sub,Thing) assert issubclass(Sub,Mixin) s = Sub.parse('') self.assertEquals(s.testit,"hello") class Sub(Mixin,Thing): pass assert issubclass(Sub,Thing) assert issubclass(Sub,Mixin) s = Sub.parse('') self.assertEquals(s.testit,42) def test_error_using_undefined_model_class(self): class Whoopsie(dexml.Model): value = fields.Model("UndefinedModel") self.assertRaises(ValueError,Whoopsie.parse,"") self.assertRaises(ValueError,Whoopsie,value=None) class Whoopsie(dexml.Model): value = fields.Model((None,"UndefinedModel")) self.assertRaises(ValueError,Whoopsie.parse,"") self.assertRaises(ValueError,Whoopsie,value=None) class Whoopsie(dexml.Model): value = fields.Model(("W:","UndefinedModel")) self.assertRaises(ValueError,Whoopsie.parse,"") self.assertRaises(ValueError,Whoopsie,value=None) def test_unordered_parse_of_list_field(self): class Notebook(dexml.Model): class meta: order_sensitive = False notes = fields.List(fields.String(tagname="note"),tagname="notes") n = Notebook.parse("onetwo") self.assertEquals(n.notes,["one","two"]) Notebook.parse("onetwo") Notebook.meta.ignore_unknown_elements = False self.assertRaises(dexml.ParseError,Notebook.parse,"onetwo") self.assertRaises(dexml.ParseError,Notebook.parse,"onetwo") dexml-0.5.1/dexml/__init__.py0000644000175000017500000005436512042330507015447 0ustar rfkrfk00000000000000""" dexml: a dead-simple Object-XML mapper for Python ================================================== Let's face it: xml is a fact of modern life. I'd even go so far as to say that it's *good* at what is does. But that doesn't mean it's easy to work with and it doesn't mean that we have to like it. Most of the time, XML just needs to get out of the way and let you do some actual work instead of writing code to traverse and manipulate yet another DOM. The dexml module takes the obvious mapping between XML tags and Python objects and lets you capture that as cleanly as possible. Loosely inspired by Django's ORM, you write simple class definitions to define the expected structure of your XML document. Like so:: >>> import dexml >>> from dexml import fields >>> class Person(dexml.Model): ... name = fields.String() ... age = fields.Integer(tagname='age') Then you can parse an XML document into an object like this:: >>> p = Person.parse("42") >>> p.name u'Foo McBar' >>> p.age 42 And you can render an object into an XML document like this:: >>> p = Person(name="Handsome B. Wonderful",age=36) >>> p.render() '36' Malformed documents will raise a ParseError:: >>> p = Person.parse("92") Traceback (most recent call last): ... ParseError: required field not found: 'name' Of course, it gets more interesting when you nest Model definitions, like this:: >>> class Group(dexml.Model): ... name = fields.String(attrname="name") ... members = fields.List(Person) ... >>> g = Group(name="Monty Python") >>> g.members.append(Person(name="John Cleese",age=69)) >>> g.members.append(Person(name="Terry Jones",age=67)) >>> g.render(fragment=True) '6967' There's support for XML namespaces, default field values, case-insensitive parsing, and more fun stuff. Check out the documentation on the following classes for more details: :Model: the base class for objects that map into XML :Field: the base class for individual model fields :Meta: meta-information about how to parse/render a model """ __ver_major__ = 0 __ver_minor__ = 5 __ver_patch__ = 1 __ver_sub__ = "" __version__ = "%d.%d.%d%s" % (__ver_major__,__ver_minor__,__ver_patch__,__ver_sub__) import sys import re import copy from xml.dom import minidom from dexml import fields if sys.version_info >= (3,): str = str #pragma: no cover unicode = str #pragma: no cover bytes = bytes #pragma: no cover basestring = (str,bytes) #pragma: no cover else: str = str #pragma: no cover unicode = unicode #pragma: no cover bytes = str #pragma: no cover basestring = basestring #pragma: no cover class Error(Exception): """Base exception class for the dexml module.""" pass class ParseError(Error): """Exception raised when XML could not be parsed into objects.""" pass class RenderError(Error): """Exception raised when object could not be rendered into XML.""" pass class XmlError(Error): """Exception raised to encapsulate errors from underlying XML parser.""" pass class PARSE_DONE: """Constant returned by a Field when it has finished parsing.""" pass class PARSE_MORE: """Constant returned by a Field when it wants additional nodes to parse.""" pass class PARSE_SKIP: """Constant returned by a Field when it cannot parse the given node.""" pass class PARSE_CHILDREN: """Constant returned by a Field to parse children from its container tag.""" pass class Meta: """Class holding meta-information about a dexml.Model subclass. Each dexml.Model subclass has an attribute 'meta' which is an instance of this class. That instance holds information about how the model corresponds to XML, such as its tagname, namespace, and error handling semantics. You would not ordinarily create an instance of this class; instead let the ModelMetaclass create one automatically. These attributes control how the model corresponds to the XML: * tagname: the name of the tag representing this model * namespace: the XML namespace in which this model lives These attributes control parsing/rendering behaviour: * namespace_prefix: the prefix to use for rendering namespaced tags * ignore_unknown_elements: ignore unknown elements when parsing * case_sensitive: match tag/attr names case-sensitively * order_sensitive: match child tags in order of field definition """ _defaults = {"tagname":None, "namespace":None, "namespace_prefix":None, "ignore_unknown_elements":True, "case_sensitive":True, "order_sensitive":True} def __init__(self,name,meta_attrs): for (attr,default) in self._defaults.items(): setattr(self,attr,meta_attrs.get(attr,default)) if self.tagname is None: self.tagname = name def _meta_attributes(meta): """Extract attributes from a "meta" object.""" meta_attrs = {} if meta: for attr in dir(meta): if not attr.startswith("_"): meta_attrs[attr] = getattr(meta,attr) return meta_attrs class ModelMetaclass(type): """Metaclass for dexml.Model and subclasses. This metaclass is responsible for introspecting Model class definitions and setting up appropriate default behaviours. For example, this metaclass sets a Model's default tagname to be equal to the declared class name. """ instances_by_tagname = {} instances_by_classname = {} def __new__(mcls,name,bases,attrs): cls = super(ModelMetaclass,mcls).__new__(mcls,name,bases,attrs) # Don't do anything if it's not a subclass of Model parents = [b for b in bases if isinstance(b, ModelMetaclass)] if not parents: return cls # Set up the cls.meta object, inheriting from base classes meta_attrs = {} for base in reversed(bases): if isinstance(base,ModelMetaclass) and hasattr(base,"meta"): meta_attrs.update(_meta_attributes(base.meta)) meta_attrs.pop("tagname",None) meta_attrs.update(_meta_attributes(attrs.get("meta",None))) cls.meta = Meta(name,meta_attrs) # Create ordered list of field objects, telling each about their # name and containing class. Inherit fields from base classes # only if not overridden on the class itself. base_fields = {} for base in bases: if not isinstance(base,ModelMetaclass): continue for field in base._fields: if field.field_name not in base_fields: field = copy.copy(field) field.model_class = cls base_fields[field.field_name] = field cls_fields = [] for (name,value) in attrs.iteritems(): if isinstance(value,fields.Field): base_fields.pop(name,None) value.field_name = name value.model_class = cls cls_fields.append(value) cls._fields = base_fields.values() + cls_fields cls._fields.sort(key=lambda f: f._order_counter) # Register the new class so we can find it by name later on tagname = (cls.meta.namespace,cls.meta.tagname) mcls.instances_by_tagname[tagname] = cls mcls.instances_by_classname[cls.__name__] = cls return cls @classmethod def find_class(mcls,tagname,namespace=None): """Find dexml.Model subclass for the given tagname and namespace.""" try: return mcls.instances_by_tagname[(namespace,tagname)] except KeyError: if namespace is None: try: return mcls.instances_by_classname[tagname] except KeyError: pass return None # You can use this re to extract the encoding declaration from the XML # document string. Hopefully you won't have to, but you might need to... _XML_ENCODING_RE = re.compile("<\\?xml [^>]*encoding=[\"']([a-zA-Z0-9\\.\\-\\_]+)[\"'][^>]*?>") class Model(object): """Base class for dexml Model objects. Subclasses of Model represent a concrete type of object that can parsed from or rendered to an XML document. The mapping to/from XML is controlled by two things: * attributes declared on an inner class named 'meta' * fields declared using instances of fields.Field Here's a quick example: class Person(dexml.Model): # This overrides the default tagname of 'Person' class meta tagname = "person" # This maps to a 'name' attributr on the tag name = fields.String() # This maps to an tag within the tag age = fields.Integer(tagname='age') See the 'Meta' class in this module for available meta options, and the 'fields' submodule for available field types. """ __metaclass__ = ModelMetaclass _fields = [] def __init__(self,**kwds): """Default Model constructor. Keyword arguments that correspond to declared fields are processed and assigned to that field. """ for f in self._fields: try: setattr(self,f.field_name,kwds[f.field_name]) except KeyError: pass @classmethod def parse(cls,xml): """Produce an instance of this model from some xml. The given xml can be a string, a readable file-like object, or a DOM node; we might add support for more types in the future. """ self = cls() node = self._make_xml_node(xml) self.validate_xml_node(node) # Keep track of fields that have successfully parsed something fields_found = [] # Try to consume all the node's attributes attrs = node.attributes.values() for field in self._fields: unused_attrs = field.parse_attributes(self,attrs) if len(unused_attrs) < len(attrs): fields_found.append(field) attrs = unused_attrs for attr in attrs: self._handle_unparsed_node(attr) # Try to consume all child nodes if self.meta.order_sensitive: self._parse_children_ordered(node,self._fields,fields_found) else: self._parse_children_unordered(node,self._fields,fields_found) # Check that all required fields have been found for field in self._fields: if field.required and field not in fields_found: err = "required field not found: '%s'" % (field.field_name,) raise ParseError(err) field.parse_done(self) # All done, return the instance so created return self def _parse_children_ordered(self,node,fields,fields_found): """Parse the children of the given node using strict field ordering.""" cur_field_idx = 0 for child in node.childNodes: idx = cur_field_idx # If we successfully break out of this loop, one of our # fields has consumed the node. while idx < len(fields): field = fields[idx] res = field.parse_child_node(self,child) if res is PARSE_DONE: if field not in fields_found: fields_found.append(field) cur_field_idx = idx + 1 break if res is PARSE_MORE: if field not in fields_found: fields_found.append(field) cur_field_idx = idx break if res is PARSE_CHILDREN: if field not in fields_found: fields_found.append(field) self._parse_children_ordered(child,[field],fields_found) cur_field_idx = idx break idx += 1 else: self._handle_unparsed_node(child) def _parse_children_unordered(self,node,fields,fields_found): """Parse the children of the given node using loose field ordering.""" done_fields = {} for child in node.childNodes: idx = 0 # If we successfully break out of this loop, one of our # fields has consumed the node. while idx < len(fields): if idx in done_fields: idx += 1 continue field = fields[idx] res = field.parse_child_node(self,child) if res is PARSE_DONE: done_fields[idx] = True if field not in fields_found: fields_found.append(field) break if res is PARSE_MORE: if field not in fields_found: fields_found.append(field) break if res is PARSE_CHILDREN: if field not in fields_found: fields_found.append(field) self._parse_children_unordered(child,[field],fields_found) break idx += 1 else: self._handle_unparsed_node(child) def _handle_unparsed_node(self,node): if not self.meta.ignore_unknown_elements: if node.nodeType == node.ELEMENT_NODE: err = "unknown element: %s" % (node.nodeName,) raise ParseError(err) elif node.nodeType in (node.TEXT_NODE,node.CDATA_SECTION_NODE): if node.nodeValue.strip(): err = "unparsed text node: %s" % (node.nodeValue,) raise ParseError(err) elif node.nodeType == node.ATTRIBUTE_NODE: if not node.nodeName.startswith("xml"): err = "unknown attribute: %s" % (node.name,) raise ParseError(err) def render(self,encoding=None,fragment=False,pretty=False,nsmap=None): """Produce XML from this model's instance data. A unicode string will be returned if any of the objects contain unicode values; specifying the 'encoding' argument forces generation of a bytestring. By default a complete XML document is produced, including the leading "" declaration. To generate an XML fragment set the 'fragment' argument to True. """ if nsmap is None: nsmap = {} data = [] if not fragment: if encoding: s = '' % (encoding,) data.append(s) else: data.append('') data.extend(self._render(nsmap)) xml = "".join(data) if pretty: xml = minidom.parseString(xml).toprettyxml() if encoding: xml = xml.encode(encoding) return xml def irender(self,encoding=None,fragment=False,nsmap=None): """Generator producing XML from this model's instance data. If any of the objects contain unicode values, the resulting output stream will be a mix of bytestrings and unicode; specify the 'encoding' arugment to force generation of bytestrings. By default a complete XML document is produced, including the leading "" declaration. To generate an XML fragment set the 'fragment' argument to True. """ if nsmap is None: nsmap = {} if not fragment: if encoding: decl = '' % (encoding,) yield decl.encode(encoding) else: yield '' if encoding: for data in self._render(nsmap): if isinstance(data,unicode): data = data.encode(encoding) yield data else: for data in self._render(nsmap): yield data def _render(self,nsmap): """Generator rendering this model as an XML fragment.""" # Determine opening and closing tags pushed_ns = False if self.meta.namespace: namespace = self.meta.namespace prefix = self.meta.namespace_prefix try: cur_ns = nsmap[prefix] except KeyError: cur_ns = [] nsmap[prefix] = cur_ns if prefix: tagname = "%s:%s" % (prefix,self.meta.tagname) open_tag_contents = [tagname] if not cur_ns or cur_ns[0] != namespace: cur_ns.insert(0,namespace) pushed_ns = True open_tag_contents.append('xmlns:%s="%s"'%(prefix,namespace)) close_tag_contents = tagname else: open_tag_contents = [self.meta.tagname] if not cur_ns or cur_ns[0] != namespace: cur_ns.insert(0,namespace) pushed_ns = True open_tag_contents.append('xmlns="%s"'%(namespace,)) close_tag_contents = self.meta.tagname else: open_tag_contents = [self.meta.tagname] close_tag_contents = self.meta.tagname used_fields = set() open_tag_contents.extend(self._render_attributes(used_fields,nsmap)) # Render each child node children = self._render_children(used_fields,nsmap) try: first_child = children.next() except StopIteration: yield "<%s />" % (" ".join(open_tag_contents),) else: yield "<%s>" % (" ".join(open_tag_contents),) yield first_child for child in children: yield child yield "" % (close_tag_contents,) # Check that all required fields actually rendered something for f in self._fields: if f.required and f not in used_fields: raise RenderError("Field '%s' is missing" % (f.field_name,)) # Clean up if pushed_ns: nsmap[prefix].pop(0) def _render_attributes(self,used_fields,nsmap): for f in self._fields: val = getattr(self,f.field_name) datas = iter(f.render_attributes(self,val,nsmap)) try: data = datas.next() except StopIteration: pass else: used_fields.add(f) yield data for data in datas: yield data def _render_children(self,used_fields,nsmap): for f in self._fields: val = getattr(self,f.field_name) datas = iter(f.render_children(self,val,nsmap)) try: data = datas.next() except StopIteration: pass else: used_fields.add(f) yield data for data in datas: yield data @staticmethod def _make_xml_node(xml): """Transform a variety of input formats to an XML DOM node.""" try: ntype = xml.nodeType except AttributeError: if isinstance(xml,bytes): try: xml = minidom.parseString(xml) except Exception, e: raise XmlError(e) elif isinstance(xml,unicode): try: # Try to grab the "encoding" attribute from the XML. # It probably won't exist, so default to utf8. encoding = _XML_ENCODING_RE.match(xml) if encoding is None: encoding = "utf8" else: encoding = encoding.group(1) xml = minidom.parseString(xml.encode(encoding)) except Exception, e: raise XmlError(e) elif hasattr(xml,"read"): try: xml = minidom.parse(xml) except Exception, e: raise XmlError(e) else: raise ValueError("Can't convert that to an XML DOM node") node = xml.documentElement else: if ntype == xml.DOCUMENT_NODE: node = xml.documentElement else: node = xml return node @classmethod def validate_xml_node(cls,node): """Check that the given xml node is valid for this object. Here 'valid' means that it is the right tag, in the right namespace. We might add more eventually... """ if node.nodeType != node.ELEMENT_NODE: err = "Class '%s' got a non-element node" err = err % (cls.__name__,) raise ParseError(err) if cls.meta.case_sensitive: if node.localName != cls.meta.tagname: err = "Class '%s' got tag '%s' (expected '%s')" err = err % (cls.__name__,node.localName, cls.meta.tagname) raise ParseError(err) else: if node.localName.lower() != cls.meta.tagname.lower(): err = "Class '%s' got tag '%s' (expected '%s')" err = err % (cls.__name__,node.localName, cls.meta.tagname) raise ParseError(err) if cls.meta.namespace: if node.namespaceURI != cls.meta.namespace: err = "Class '%s' got namespace '%s' (expected '%s')" err = err % (cls.__name__,node.namespaceURI, cls.meta.namespace) raise ParseError(err) else: if node.namespaceURI: err = "Class '%s' got namespace '%s' (expected no namespace)" err = err % (cls.__name__,node.namespaceURI,) raise ParseError(err) dexml-0.5.1/dexml/fields.py0000644000175000017500000007310612042330375015153 0ustar rfkrfk00000000000000""" dexml.fields: basic field type definitions for dexml ===================================================== """ import dexml import random from xml.sax.saxutils import escape, quoteattr # Global counter tracking the order in which fields are declared. _order_counter = 0 class _AttrBucket: """A simple class used only to hold attributes.""" pass class Field(object): """Base class for all dexml Field classes. Field classes are responsible for parsing and rendering individual components to the XML. They also act as descriptors on dexml Model instances, to get/set the corresponding properties. Each field instance will magically be given the following properties: * model_class: the Model subclass to which it is attached * field_name: the name under which is appears on that class The following methods are required for interaction with the parsing and rendering machinery: * parse_attributes: parse info out of XML node attributes * parse_child_node: parse into out of an XML child node * render_attributes: render XML for node attributes * render_children: render XML for child nodes """ class arguments: required = True def __init__(self,**kwds): """Default Field constructor. This constructor keeps track of the order in which Field instances are created, since this information can have semantic meaning in XML. It also merges any keyword arguments with the defaults defined on the 'arguments' inner class, and assigned these attributes to the Field instance. """ global _order_counter self._order_counter = _order_counter = _order_counter + 1 args = self.__class__.arguments for argnm in dir(args): if not argnm.startswith("__"): setattr(self,argnm,kwds.get(argnm,getattr(args,argnm))) def parse_attributes(self,obj,attrs): """Parse any attributes for this field from the given list. This method will be called with the Model instance being parsed and a list of attribute nodes from its XML tag. Any attributes of interest to this field should be processed, and a list of the unused attribute nodes returned. """ return attrs def parse_child_node(self,obj,node): """Parse a child node for this field. This method will be called with the Model instance being parsed and the current child node of that model's XML tag. There are three options for processing this node: * return PARSE_DONE, indicating that it was consumed and this field now has all the necessary data. * return PARSE_MORE, indicating that it was consumed but this field will accept more nodes. * return PARSE_SKIP, indicating that it was not consumed by this field. Any other return value will be taken as a parse error. """ return dexml.PARSE_SKIP def parse_done(self,obj): """Finalize parsing for the given object. This method is called as a simple indicator that no more data will be forthcoming. No return value is expected. """ pass def render_attributes(self,obj,val,nsmap): """Render any attributes that this field manages.""" return [] def render_children(self,obj,nsmap,val): """Render any child nodes that this field manages.""" return [] def __get__(self,instance,owner=None): if instance is None: return self return instance.__dict__.get(self.field_name) def __set__(self,instance,value): instance.__dict__[self.field_name] = value def _check_tagname(self,node,tagname): if node.nodeType != node.ELEMENT_NODE: return False if isinstance(tagname,basestring): if node.localName != tagname: return False if node.namespaceURI: if node.namespaceURI != self.model_class.meta.namespace: return False else: (tagns,tagname) = tagname if node.localName != tagname: return False if node.namespaceURI != tagns: return False return True class Value(Field): """Field subclass that holds a simple scalar value. This Field subclass contains the common logic to parse/render simple scalar value fields - fields that don't required any recursive parsing. Individual subclasses should provide the parse_value() and render_value() methods to do type coercion of the value. Value fields can also have a default value, specified by the 'default' keyword argument. By default, the field maps to an attribute of the model's XML node with the same name as the field declaration. Consider: class MyModel(Model): my_field = fields.Value(default="test") This corresponds to the XML fragment "". To use a different name specify the 'attrname' kwd argument. To use a subtag instead of an attribute specify the 'tagname' kwd argument. Namespaced attributes or subtags are also supported, by specifying a (namespace,tagname) pair for 'attrname' or 'tagname' respectively. """ class arguments(Field.arguments): tagname = None attrname = None default = None def __init__(self,**kwds): super(Value,self).__init__(**kwds) if self.default is not None: self.required = False def _get_attrname(self): if self.__dict__["tagname"]: return None attrname = self.__dict__['attrname'] if not attrname: attrname = self.field_name return attrname def _set_attrname(self,attrname): self.__dict__['attrname'] = attrname attrname = property(_get_attrname,_set_attrname) def _get_tagname(self): if self.__dict__["attrname"]: return None tagname = self.__dict__['tagname'] if tagname and not isinstance(tagname,(basestring,tuple)): tagname = self.field_name return tagname def _set_tagname(self,tagname): self.__dict__['tagname'] = tagname tagname = property(_get_tagname,_set_tagname) def __get__(self,instance,owner=None): val = super(Value,self).__get__(instance,owner) if val is None: return self.default return val def parse_attributes(self,obj,attrs): # Bail out if we're attached to a subtag rather than an attr. if self.tagname: return attrs unused = [] attrname = self.attrname if isinstance(attrname,basestring): ns = None else: (ns,attrname) = attrname for attr in attrs: if attr.localName == attrname: if attr.namespaceURI == ns: self.__set__(obj,self.parse_value(attr.nodeValue)) else: unused.append(attr) else: unused.append(attr) return unused def parse_child_node(self,obj,node): if not self.tagname: return dexml.PARSE_SKIP if self.tagname == ".": node = node.parentNode else: if not self._check_tagname(node,self.tagname): return dexml.PARSE_SKIP vals = [] # Merge all text nodes into a single value for child in node.childNodes: if child.nodeType not in (child.TEXT_NODE,child.CDATA_SECTION_NODE): raise dexml.ParseError("non-text value node") vals.append(child.nodeValue) self.__set__(obj,self.parse_value("".join(vals))) return dexml.PARSE_DONE def render_attributes(self,obj,val,nsmap): if val is not None and val is not self.default and self.attrname: qaval = quoteattr(self.render_value(val)) if isinstance(self.attrname,basestring): yield '%s=%s' % (self.attrname,qaval,) else: m_meta = self.model_class.meta (ns,nm) = self.attrname if ns == m_meta.namespace and m_meta.namespace_prefix: prefix = m_meta.namespace_prefix yield '%s:%s=%s' % (prefix,nm,qaval,) elif ns is None: yield '%s=%s' % (nm,qaval,) else: for (p,n) in nsmap.iteritems(): if ns == n[0]: prefix = p break else: prefix = "p" + str(random.randint(0,10000)) while prefix in nsmap: prefix = "p" + str(random.randint(0,10000)) yield 'xmlns:%s="%s"' % (prefix,ns,) yield '%s:%s=%s' % (prefix,nm,qaval,) def render_children(self,obj,val,nsmap): if val is not None and val is not self.default and self.tagname: val = self._esc_render_value(val) if self.tagname == ".": yield val else: attrs = "" # By default, tag values inherit the namespace of their # containing model class. if isinstance(self.tagname,basestring): prefix = self.model_class.meta.namespace_prefix localName = self.tagname else: m_meta = self.model_class.meta (ns,localName) = self.tagname if not ns: # If we have an explicitly un-namespaced tag, # we need to be careful. The model tag might have # set the default namespace, which we need to undo. prefix = None if m_meta.namespace and not m_meta.namespace_prefix: attrs = ' xmlns=""' elif ns == m_meta.namespace: prefix = m_meta.namespace_prefix else: for (p,n) in nsmap.iteritems(): if ns == n[0]: prefix = p break else: prefix = "p" + str(random.randint(0,10000)) while prefix in nsmap: prefix = "p" + str(random.randint(0,10000)) attrs = ' xmlns:%s="%s"' % (prefix,ns) yield self._render_tag(val,prefix,localName,attrs) def _render_tag(self,val,prefix,localName,attrs): if val: if prefix: args = (prefix,localName,attrs,val,prefix,localName) return "<%s:%s%s>%s" % args else: return "<%s%s>%s" % (localName,attrs,val,localName) else: if prefix: return "<%s:%s%s />" % (prefix,localName,attrs,) else: return "<%s%s />" % (localName,attrs) def parse_value(self,val): return val def render_value(self,val): if not isinstance(val, basestring): val = str(val) return val def _esc_render_value(self,val): return escape(self.render_value(val)) class String(Value): """Field representing a simple string value.""" # actually, the base Value() class will do this automatically. pass class CDATA(Value): """String field rendered as CDATA.""" def __init__(self,**kwds): super(CDATA,self).__init__(**kwds) if self.__dict__.get("tagname",None) is None: raise ValueError("CDATA fields must have a tagname") def _esc_render_value(self,val): val = self.render_value(val) val = val.replace("]]>","]]]]>") return "" class Integer(Value): """Field representing a simple integer value.""" def parse_value(self,val): return int(val) class Float(Value): """Field representing a simple float value.""" def parse_value(self,val): return float(val) class Boolean(Value): """Field representing a simple boolean value. The strings corresponding to false are 'no', 'off', 'false' and '0', compared case-insensitively. Note that this means an empty tag or attribute is considered True - this is usually what you want, since a completely missing attribute or tag can be interpreted as False. To enforce that the presence of a tag indicates True and the absence of a tag indicates False, pass the keyword argument "empty_only". """ class arguments(Value.arguments): empty_only = False def __init__(self,**kwds): super(Boolean,self).__init__(**kwds) if self.empty_only: self.required = False def __set__(self,instance,value): instance.__dict__[self.field_name] = bool(value) def parse_value(self,val): if self.empty_only and val != "": raise ValueError("non-empty value in empty_only Boolean") if val.lower() in ("no","off","false","0"): return False return True def render_children(self,obj,val,nsmap): if not val and self.empty_only: return [] return super(Boolean,self).render_children(obj,val,nsmap) def render_attributes(self,obj,val,nsmap): if not val and self.empty_only: return [] return super(Boolean,self).render_attributes(obj,val,nsmap) def render_value(self,val): if not val: return "false" if self.empty_only: return "" return "true" class Model(Field): """Field subclass referencing another Model instance. This field sublcass allows Models to contain other Models recursively. The first argument to the field constructor must be either a Model class, or the name or tagname of a Model class. """ class arguments(Field.arguments): type = None def __init__(self,type=None,**kwds): kwds["type"] = type super(Model,self).__init__(**kwds) def _get_type(self): return self.__dict__.get("type") def _set_type(self,value): if value is not None: self.__dict__["type"] = value type = property(_get_type,_set_type) def __set__(self,instance,value): typeclass = self.typeclass if value and not isinstance(value, typeclass): raise ValueError("Invalid value type %s. Model field requires %s instance" % (value.__class__.__name__, typeclass.__name__)) super(Model, self).__set__(instance, value) @property def typeclass(self): try: return self.__dict__['typeclass'] except KeyError: self.__dict__['typeclass'] = self._load_typeclass() return self.__dict__['typeclass'] def _load_typeclass(self): typ = self.type if isinstance(typ,dexml.ModelMetaclass): return typ if typ is None: typ = self.field_name typeclass = None if isinstance(typ,basestring): if self.model_class.meta.namespace: ns = self.model_class.meta.namespace typeclass = dexml.ModelMetaclass.find_class(typ,ns) if typeclass is None: typeclass = dexml.ModelMetaclass.find_class(typ,None) if typeclass is None: raise ValueError("Unknown Model class: %s" % (typ,)) else: (ns,typ) = typ if isinstance(typ,dexml.ModelMetaclass): return typ typeclass = dexml.ModelMetaclass.find_class(typ,ns) if typeclass is None: raise ValueError("Unknown Model class: (%s,%s)" % (ns,typ)) return typeclass def parse_child_node(self,obj,node): typeclass = self.typeclass try: typeclass.validate_xml_node(node) except dexml.ParseError: return dexml.PARSE_SKIP else: inst = typeclass.parse(node) self.__set__(obj,inst) return dexml.PARSE_DONE def render_attributes(self,obj,val,nsmap): return [] def render_children(self,obj,val,nsmap): if val is not None: for data in val._render(nsmap): yield data class List(Field): """Field subclass representing a list of fields. This field corresponds to a homogenous list of other fields. You would declare it like so: class MyModel(Model): items = fields.List(fields.String(tagname="item")) Corresponding to XML such as: onetwo The properties 'minlength' and 'maxlength' control the allowable length of the list. The 'tagname' property sets an optional wrapper tag which acts as container for list items, for example: class MyModel(Model): items = fields.List(fields.String(tagname="item"), tagname='list') Corresponding to XML such as: onetwo This wrapper tag is always rendered, even if the list is empty. It is transparently removed when parsing. """ class arguments(Field.arguments): field = None minlength = None maxlength = None tagname = None def __init__(self,field,**kwds): if isinstance(field,Field): kwds["field"] = field else: kwds["field"] = Model(field,**kwds) super(List,self).__init__(**kwds) if not self.minlength and not self.tagname: self.required = False if self.minlength and not self.required: raise ValueError("List must be required if it has minlength") def _get_field(self): field = self.__dict__["field"] if not hasattr(field,"field_name"): field.field_name = self.field_name if not hasattr(field,"model_class"): field.model_class = self.model_class return field def _set_field(self,field): self.__dict__["field"] = field field = property(_get_field,_set_field) def __get__(self,instance,owner=None): val = super(List,self).__get__(instance,owner) if val is not None: return val self.__set__(instance,[]) return self.__get__(instance,owner) def parse_child_node(self,obj,node): # If our children are inside a grouping tag, parse # that first. The presence of this is indicated by # setting the empty list on the target object. if self.tagname: val = super(List,self).__get__(obj) if val is None: if node.nodeType != node.ELEMENT_NODE: return dexml.PARSE_SKIP elif node.tagName == self.tagname: self.__set__(obj,[]) return dexml.PARSE_CHILDREN else: return dexml.PARSE_SKIP # Now we just parse each child node. tmpobj = _AttrBucket() res = self.field.parse_child_node(tmpobj,node) if res is dexml.PARSE_MORE: raise ValueError("items in a list cannot return PARSE_MORE") if res is dexml.PARSE_DONE: items = self.__get__(obj) val = getattr(tmpobj,self.field_name) items.append(val) return dexml.PARSE_MORE else: return dexml.PARSE_SKIP def parse_done(self,obj): items = self.__get__(obj) if self.minlength is not None and len(items) < self.minlength: raise dexml.ParseError("Field '%s': not enough items" % (self.field_name,)) if self.maxlength is not None and len(items) > self.maxlength: raise dexml.ParseError("Field '%s': too many items" % (self.field_name,)) def render_children(self,obj,items,nsmap): # Create a generator that yields child data chunks, and validates # the number of items in the list as it goes. It allows any # iterable to be passed in, not just a list. def child_chunks(): num_items = 0 for item in items: num_items += 1 if self.maxlength is not None and num_items > self.maxlength: msg = "Field '%s': too many items" % (self.field_name,) raise dexml.RenderError(msg) for data in self.field.render_children(obj,item,nsmap): yield data if self.minlength is not None and num_items < self.minlength: msg = "Field '%s': not enough items" % (self.field_name,) raise dexml.RenderError(msg) chunks = child_chunks() # Render each chunk, but suppress the wrapper tag if there's no data. try: data = chunks.next() except StopIteration: if self.tagname and self.required: yield "<%s />" % (self.tagname,) else: if self.tagname: yield "<%s>" % (self.tagname,) yield data for data in chunks: yield data if self.tagname: yield "" % (self.tagname,) class Dict(Field): """Field subclass representing a dict of fields keyed by unique attribute value. This field corresponds to an indexed dict of other fields. You would declare it like so: class MyObject(Model): name = fields.String(tagname = 'name') attr = fields.String(tagname = 'attr') class MyModel(Model): items = fields.Dict(fields.Model(MyObject), key = 'name') Corresponding to XML such as: obj1val1 The properties 'minlength' and 'maxlength' control the allowable size of the dict as in the List class. If 'unique' property is set to True, parsing will raise exception on non-unique key values. The 'dictclass' property controls the internal dict-like class used by the fielt. By default it is the standard dict class. The 'tagname' property sets the 'wrapper' tag which acts as container for dict items, for example: from collections import defaultdict class MyObject(Model): name = fields.String() attr = fields.String() class MyDict(defaultdict): def __init__(self): super(MyDict, self).__init__(MyObject) class MyModel(Model): objects = fields.Dict('MyObject', key = 'name', tagname = 'dict', dictclass = MyDict) xml = ''\ val1' mymodel = MyModel.parse(xml) obj2 = mymodel['obj2'] print(obj2.name) print(mymodel.render(fragment = True)) This wrapper tag is always rendered, even if the dict is empty. It is transparently removed when parsing. """ class arguments(Field.arguments): field = None minlength = None maxlength = None unique = False tagname = None dictclass = dict def __init__(self, field, key, **kwds): if isinstance(field, Field): kwds["field"] = field else: kwds["field"] = Model(field, **kwds) super(Dict, self).__init__(**kwds) if not self.minlength and not self.tagname: self.required = False if self.minlength and not self.required: raise ValueError("Dict must be required if it has minlength") self.key = key def _get_field(self): field = self.__dict__["field"] if not hasattr(field, "field_name"): field.field_name = self.field_name if not hasattr(field, "model_class"): field.model_class = self.model_class return field def _set_field(self, field): self.__dict__["field"] = field field = property(_get_field, _set_field) def __get__(self,instance,owner=None): val = super(Dict, self).__get__(instance, owner) if val is not None: return val class dictclass(self.dictclass): key = self.key def __setitem__(self, key, value): keyval = getattr(value, self.key) if keyval and keyval != key: raise ValueError('Key field value does not match dict key') setattr(value, self.key, key) super(dictclass, self).__setitem__(key, value) self.__set__(instance, dictclass()) return self.__get__(instance, owner) def parse_child_node(self, obj, node): # If our children are inside a grouping tag, parse # that first. The presence of this is indicated by # setting an empty dict on the target object. if self.tagname: val = super(Dict,self).__get__(obj) if val is None: if node.nodeType != node.ELEMENT_NODE: return dexml.PARSE_SKIP elif node.tagName == self.tagname: self.__get__(obj) return dexml.PARSE_CHILDREN else: return dexml.PARSE_SKIP # Now we just parse each child node. tmpobj = _AttrBucket() res = self.field.parse_child_node(tmpobj, node) if res is dexml.PARSE_MORE: raise ValueError("items in a dict cannot return PARSE_MORE") if res is dexml.PARSE_DONE: items = self.__get__(obj) val = getattr(tmpobj, self.field_name) try: key = getattr(val, self.key) except AttributeError: raise dexml.ParseError("Key field '%s' required but not found in dict value" % (self.key, )) if self.unique and key in items: raise dexml.ParseError("Key '%s' already exists in dict" % (key,)) items[key] = val return dexml.PARSE_MORE else: return dexml.PARSE_SKIP def parse_done(self, obj): items = self.__get__(obj) if self.minlength is not None and len(items) < self.minlength: raise dexml.ParseError("Field '%s': not enough items" % (self.field_name,)) if self.maxlength is not None and len(items) > self.maxlength: raise dexml.ParseError("Field '%s': too many items" % (self.field_name,)) def render_children(self, obj, items, nsmap): if self.minlength is not None and len(items) < self.minlength: raise dexml.RenderError("Field '%s': not enough items" % (self.field_name,)) if self.maxlength is not None and len(items) > self.maxlength: raise dexml.RenderError("too many items") if self.tagname: children = "".join(data for item in items.values() for data in self.field.render_children(obj,item,nsmap)) if not children: if self.required: yield "<%s />" % (self.tagname,) else: yield children.join(('<%s>'%self.tagname, ''%self.tagname)) else: for item in items.values(): for data in self.field.render_children(obj, item, nsmap): yield data class Choice(Field): """Field subclass accepting any one of a given set of Model fields.""" class arguments(Field.arguments): fields = [] def __init__(self,*fields,**kwds): real_fields = [] for field in fields: if isinstance(field,Model): real_fields.append(field) elif isinstance(field,basestring): real_fields.append(Model(field)) else: raise ValueError("only Model fields are allowed within a Choice field") kwds["fields"] = real_fields super(Choice,self).__init__(**kwds) def parse_child_node(self,obj,node): for field in self.fields: field.field_name = self.field_name field.model_class = self.model_class res = field.parse_child_node(obj,node) if res is dexml.PARSE_MORE: raise ValueError("items in a Choice cannot return PARSE_MORE") if res is dexml.PARSE_DONE: return dexml.PARSE_DONE else: return dexml.PARSE_SKIP def render_children(self,obj,item,nsmap): if item is None: if self.required: raise dexml.RenderError("Field '%s': required field is missing" % (self.field_name,)) else: for data in item._render(nsmap=nsmap): yield data class XmlNode(Field): class arguments(Field.arguments): tagname = None encoding = None def __set__(self,instance,value): if isinstance(value,basestring): if isinstance(value,unicode) and self.encoding: value = value.encode(self.encoding) doc = dexml.minidom.parseString(value) value = doc.documentElement if value is not None and value.namespaceURI is not None: nsattr = "xmlns" if value.prefix: nsattr = ":".join((nsattr,value.prefix,)) value.attributes[nsattr] = value.namespaceURI return super(XmlNode,self).__set__(instance,value) def parse_child_node(self,obj,node): if self.tagname is None or self._check_tagname(node,self.tagname): self.__set__(obj,node) return dexml.PARSE_DONE return dexml.PARSE_SKIP @classmethod def render_children(cls,obj,val,nsmap): if val is not None: yield val.toxml() dexml-0.5.1/ChangeLog.txt0000644000175000017500000000636512042330526014613 0ustar rfkrfk00000000000000 v0.5.0 * Fix a broken setup.py file. v0.5.0 * Implemented a basic project website, with beginnings of tutorial and API docs. * Implement Model.render(pretty=True) for pretty-printed XML output. * Fix rendering of unicode String fields; thanks @saltycrane. * Clean up and clarify handling of wrapper tags for List/Dict fields. When the "tagname" property is specified: * If the field is required then the wrapper tag is required, even if it has no contents. * If the field is required then the wrapper tag is always rendered, even if it has no contents. * If the field is not required then the wrapper tag can be elided, and will not be rendered when the collection is empty. Behaviour of List/Dict without a wrapper tag is unchanged. v0.4.2 * Got 100% test coverage on all target python versions. Many edge-case bugs squashed in the process. * Eliminate all use of "raise RuntimeError"; use ValueError instead because they were all about incorrect values encountered at runtime. * Inherit meta-attributes from base classes in left-to-right order. * fields.Boolean: coerce all values to a proper bool. v0.4.1 * Fix handling of whitespace around List/Dict fields; thanks Alexey Luchko. v0.4.0 * Some API changes to allow generator-based rendering: * Model.irender() is a generator version of Model.render() * Model.render() no longer takes the "nsmap" argument * fields.Model no longer calls Model.render(), so reimplementing it in a subclass probably won't work; reimplement Model._render() instead. * fields.List accepts a generator, and won't consume it until rendered. * fields.Model: when specifying the model class as a string, search first by tagname and then by classname. * fields.Value: allow a tagname of "." to parse a value directly out of the contents of the parent node. v0.3.7 * Automatically escape/unescape special characters in string fields. * Parse CDATA_SECTION nodes as if they were TEXT nodes. * Added CDATA field type, which is just like a String except it renders as a CDATA_SECTION node. v0.3.6 * strict type checking for Model fields; thanks Alexander Vladimirov. v0.3.5: * generate fewer redundant namespace prefixes; thanks José Orlando Pereira. v0.3.4: * allow Model subclasses to inherit meta attributes from their base classes. (again thanks to Alexander Vladimirov) * allow List and Dict fields to specify a "wrapper" tag that groups the contained items. This tag doesn't show up in the object structure, it is added and removed transparently during rendering and parsing. v0.3.3: * Dict field type; thanks to Alexander Vladimirov. v0.3.2: * support for order-insensitive parsing * more careful management of namespaces in fields.XmlNode. * support for rendering fields whose attrname or tagname is a tuple (namespace,localName) v0.3.1: * support for empty-only Boolean fields, where presence of the attribute or tag indicates True and absence indicates False. v0.3.0: * support for Python 3, via distribute and the "use_2to3" flag v0.2.1: * support for optional Choice fields, thanks to Jose Orlando Pereira v0.2.0: * changed license from BSD to MIT (it's simpler) dexml-0.5.1/LICENSE.txt0000644000175000017500000000204311723311564014041 0ustar rfkrfk00000000000000Copyright (c) 2009-2011 Ryan Kelly Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dexml-0.5.1/setup.cfg0000644000175000017500000000007312042330626014033 0ustar rfkrfk00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 dexml-0.5.1/README.rst0000644000175000017500000000447611723311563013720 0ustar rfkrfk00000000000000 dexml: a dead-simple Object-XML mapper for Python ================================================== Let's face it: xml is a fact of modern life. I'd even go so far as to say that it's *good* at what is does. But that doesn't mean it's easy to work with and it doesn't mean that we have to like it. Most of the time, XML just needs to get out of the way and let you do some actual work instead of writing code to traverse and manipulate yet another DOM. The dexml module takes the obvious mapping between XML tags and Python objects and lets you capture that as cleanly as possible. Loosely inspired by Django's ORM, you write simple class definitions to define the expected structure of your XML document. Like so:: >>> import dexml >>> from dexml import fields >>> class Person(dexml.Model): ... name = fields.String() ... age = fields.Integer(tagname='age') Then you can parse an XML document into an object like this:: >>> p = Person.parse("42") >>> p.name u'Foo McBar' >>> p.age 42 And you can render an object into an XML document like this:: >>> p = Person(name="Handsome B. Wonderful",age=36) >>> p.render() '36' Malformed documents will raise a ParseError:: >>> p = Person.parse("92") Traceback (most recent call last): ... ParseError: required field not found: 'name' Of course, it gets more interesting when you nest Model definitions, like this:: >>> class Group(dexml.Model): ... name = fields.String(attrname="name") ... members = fields.List(Person) ... >>> g = Group(name="Monty Python") >>> g.members.append(Person(name="John Cleese",age=69)) >>> g.members.append(Person(name="Terry Jones",age=67)) >>> g.render(fragment=True) '6967' There's support for XML namespaces, default field values, case-insensitive parsing, and more fun stuff. Check out the documentation on the following classes for more details: :Model: the base class for objects that map into XML :Field: the base class for individual model fields :Meta: meta-information about how to parse/render a model