dexml-0.5.1/ 0000755 0001750 0001750 00000000000 12042330626 012212 5 ustar rfk rfk 0000000 0000000 dexml-0.5.1/setup.py 0000644 0001750 0001750 00000004451 12042330443 013725 0 ustar rfk rfk 0000000 0000000 #
# 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.in 0000644 0001750 0001750 00000000077 11723311557 013763 0 ustar rfk rfk 0000000 0000000
include README.rst
include LICENSE.txt
include ChangeLog.txt
dexml-0.5.1/PKG-INFO 0000644 0001750 0001750 00000007312 12042330626 013312 0 ustar rfk rfk 0000000 0000000 Metadata-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/ 0000755 0001750 0001750 00000000000 12042330626 015015 5 ustar rfk rfk 0000000 0000000 dexml-0.5.1/dexml.egg-info/SOURCES.txt 0000644 0001750 0001750 00000000335 12042330623 016677 0 ustar rfk rfk 0000000 0000000 ChangeLog.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.txt dexml-0.5.1/dexml.egg-info/PKG-INFO 0000644 0001750 0001750 00000007312 12042330623 016112 0 ustar rfk rfk 0000000 0000000 Metadata-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.txt 0000644 0001750 0001750 00000000006 12042330623 017540 0 ustar rfk rfk 0000000 0000000 dexml
dexml-0.5.1/dexml.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 12042330623 021060 0 ustar rfk rfk 0000000 0000000
dexml-0.5.1/dexml/ 0000755 0001750 0001750 00000000000 12042330626 013323 5 ustar rfk rfk 0000000 0000000 dexml-0.5.1/dexml/test.py 0000644 0001750 0001750 00000127303 12042330375 014663 0 ustar rfk rfk 0000000 0000000 """
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 = '- 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
')
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 = '- val1
- val2
'
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__.py 0000644 0001750 0001750 00000054365 12042330507 015447 0 ustar rfk rfk 0000000 0000000 """
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 "%s>" % (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.py 0000644 0001750 0001750 00000073106 12042330375 015153 0 ustar rfk rfk 0000000 0000000 """
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%s:%s>" % args
else:
return "<%s%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:
- one
- two
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:
- one
- two
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 "%s>" % (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, '%s>'%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.txt 0000644 0001750 0001750 00000006365 12042330526 014613 0 ustar rfk rfk 0000000 0000000
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.txt 0000644 0001750 0001750 00000002043 11723311564 014041 0 ustar rfk rfk 0000000 0000000 Copyright (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.cfg 0000644 0001750 0001750 00000000073 12042330626 014033 0 ustar rfk rfk 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
dexml-0.5.1/README.rst 0000644 0001750 0001750 00000004476 11723311563 013720 0 ustar rfk rfk 0000000 0000000
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