Skip to content

Commit

Permalink
Revise ActiveResource to handle STI types better. It will now create
Browse files Browse the repository at this point in the history
subclass objects when parsing XML. Added element_containers to support this in
a clean manner.



git-svn-id: http://pyactiveresource.googlecode.com/svn/trunk@77 8807a2d2-0055-0410-b418-2ff1661b7dac
  • Loading branch information
Daniel Van Derveer committed Oct 12, 2010
1 parent 5357b75 commit 73440f6
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 35 deletions.
5 changes: 4 additions & 1 deletion src/activeresource.py
Expand Up @@ -12,6 +12,7 @@
import urlparse
from string import Template
from pyactiveresource import connection
from pyactiveresource import element_containers
from pyactiveresource import formats
from pyactiveresource import util

Expand Down Expand Up @@ -85,7 +86,7 @@ def from_xml(self, xml_string):
attribute_keys = self.base.attributes.keys()
try:
messages = util.xml_to_dict(
xml_string, saveroot=True)['errors']['error']
xml_string)['errors']['error']
if not isinstance(messages, list):
messages = [messages]
except util.Error:
Expand Down Expand Up @@ -518,6 +519,8 @@ def _build_list(cls, elements, prefix_options=None):
A list of ActiveResource objects.
"""
resources = []
if isinstance(elements, dict):
elements = [elements]
# slice elements to ensure that this is a list-type object not a dict
for element in elements[:]:
resources.append(cls(element, prefix_options))
Expand Down
22 changes: 22 additions & 0 deletions src/element_containers.py
@@ -0,0 +1,22 @@
#!/usr/bin/python2.4
#
# Copyright 2010 Google Inc. All Rights Reserved.

__author__ = 'danv@google.com (Daniel Van Derveer)'

class ElementList(list):
"""A list object with an element_type attribute."""

def __init__(self, element_type, *args):
"""Constructor for ElementList."""
self.element_type = element_type
super(ElementList, self).__init__(*args)


class ElementDict(dict):
"""A dictionary object with an element_type attribute."""

def __init__(self, element_type, *args):
"""Constructor for ElementDict."""
self.element_type = element_type
super(ElementDict, self).__init__(*args)
10 changes: 4 additions & 6 deletions src/formats.py
Expand Up @@ -5,7 +5,6 @@

__author__ = 'Mark Roach (mrroach@google.com)'


import logging
from pyactiveresource import util

Expand All @@ -16,24 +15,23 @@ class Error(Exception):

class Base(object):
"""A base format object for inheritance."""


class XMLFormat(Base):
"""Read XML formatted ActiveResource objects."""

extension = 'xml'
mime_type = 'application/xml'

@staticmethod
def decode(resource_string):
"""Convert a resource string to a dictionary."""
log = logging.getLogger('pyactiveresource.format')
log.debug('decoding resource: %s', resource_string)
try:
data = util.xml_to_dict(resource_string, saveroot=True)
data = util.xml_to_dict(resource_string, saveroot=False)
except util.Error, err:
raise Error(err)
if isinstance(data, dict) and len(data) == 1:
data = data.values()[0]
return data

9 changes: 6 additions & 3 deletions src/tests/activeresource_test.py
Expand Up @@ -94,14 +94,16 @@ def test_find_parses_non_array_collection(self):
<person><name>jim</name><id>2</id></person>
</people>'''
self.http.respond_to('GET', '/people.xml', {}, collection_xml)
self.assertRaises(Exception, self.person.find)
results = self.person.find()
self.assertEqual(2, len(results))

def test_find_parses_single_item_non_array_collection(self):
collection_xml = '''<people>
<person><name>jim</name><id>2</id></person>
</people>'''
self.http.respond_to('GET', '/people.xml', {}, collection_xml)
self.assertRaises(Exception, self.person.find)
results = self.person.find()
self.assertEqual(1, len(results))

def test_find_by_id(self):
# Return a single person for a find(id=<id>) call
Expand Down Expand Up @@ -457,7 +459,8 @@ def test_to_xml_should_handle_attributes_containing_lists_of_dicts(self):
res = activeresource.ActiveResource()
res.children = children
xml = res.to_xml()
self.assertEqual(children, util.xml_to_dict(xml)['children'])
parsed = util.xml_to_dict(xml, saveroot=False)
self.assertEqual(children, parsed['children'])

def test_to_xml_should_handle_dasherize_option(self):
res = activeresource.ActiveResource({'attr_name': 'value'})
Expand Down
43 changes: 40 additions & 3 deletions src/tests/util_test.py
Expand Up @@ -60,9 +60,9 @@ def test_xml_to_dict_single_record(self):
'optimum_viewing_angle': 135.0,
'resident': 'yes'}

self.assertEqual(expected_topic_dict, util.xml_to_dict(topic_xml))
self.assertEqual(expected_topic_dict, util.xml_to_dict(topic_xml, saveroot=False))
self.assertEqual(expected_topic_dict,
util.xml_to_dict(topic_xml, saveroot=True)['topic'])
util.xml_to_dict(topic_xml)['topic'])

def test_xml_to_dict_multiple_records(self):
"""Test the xml to dict function."""
Expand Down Expand Up @@ -109,11 +109,48 @@ def test_xml_to_dict_multiple_records(self):
'parent_id': None}

self.assertEqual(expected_topic_dict,
util.xml_to_dict(topics_xml)[0])
util.xml_to_dict(topics_xml, saveroot=False)[0])
self.assertEqual(
expected_topic_dict,
util.xml_to_dict(topics_xml, saveroot=True)['topics'][0])

def test_xml_to_dict_multiple_records_with_different_types(self):
"""Test the xml to dict function."""
topics_xml = '''<topics type="array">
<topic>
<title>The First Topic</title>
<author-name>David</author-name>
<id type="integer">1</id>
<approved type="boolean">false</approved>
<replies-count type="integer">0</replies-count>
<replies-close-in type="integer">2592000000</replies-close-in>
<written-on type="date">2003-07-16</written-on>
<viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
<content>Have a nice day</content>
<author-email-address>david@loudthinking.com</author-email-address>
<parent-id nil="true"></parent-id>
</topic>
<topic type='SubTopic'>
<title>The Second Topic</title>
<author-name>Jason</author-name>
<id type="integer">1</id>
<approved type="boolean">false</approved>
<replies-count type="integer">0</replies-count>
<replies-close-in type="integer">2592000000</replies-close-in>
<written-on type="date">2003-07-16</written-on>
<viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
<content>Have a nice day</content>
<author-email-address>david@loudthinking.com</author-email-address>
<parent-id></parent-id>
</topic>
</topics>'''

parsed = util.xml_to_dict(topics_xml, saveroot=False)
self.assertEqual('topics', parsed.element_type)
self.assertEqual('topic', parsed[0].element_type)
self.assertEqual('sub_topic', parsed[1].element_type)


def test_xml_to_dict_empty_array(self):
blog_xml = '''<blog>
<posts type="array"></posts>
Expand Down
44 changes: 22 additions & 22 deletions src/util.py
Expand Up @@ -8,6 +8,7 @@
import base64
import calendar
import decimal
import element_containers
import re
import time
import datetime
Expand Down Expand Up @@ -43,7 +44,7 @@ def date_parse(time_string):
try:
from xml.etree import ElementTree as ET
except ImportError:
from elementtree import ElementTree as ET
from google3.third_party.python.elementtree import ElementTree as ET

XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>'

Expand Down Expand Up @@ -280,14 +281,14 @@ def to_xml(obj, root='object', pretty=False, header=True, dasherize=True):
return xml_data


def xml_to_dict(xmlobj, saveroot=False):
def xml_to_dict(xmlobj, saveroot=True):
"""Parse the xml into a dictionary of attributes.
Args:
xmlobj: An ElementTree element or an xml string.
saveroot: Keep the xml element names (ugly format)
Returns:
A dictionary of attributes (possibly nested).
An ElementDict object or ElementList for multiple objects
"""
if isinstance(xmlobj, basestring):
# Allow for blank (usually HEAD) result on success
Expand All @@ -302,22 +303,20 @@ def xml_to_dict(xmlobj, saveroot=False):

element_type = element.get('type', '').lower()
if element_type == 'array':
# This is a list, build either a list, or an array like:
# {list_element_type: [list_element,...]}
element_list_type = element.tag.replace('-', '_')
return_list = element_containers.ElementList(element_list_type)
for child in element.getchildren():
child_element = xml_to_dict(child, saveroot)
if saveroot and isinstance(child_element, dict):
return_list.append(child_element.values()[0])
else:
return_list.append(child_element)
if saveroot:
child_tag = singularize(element.tag.replace('-', '_'))
attributes = []
for child in element.getchildren():
attribute = xml_to_dict(child, saveroot)
if isinstance(attribute, dict):
attribute = attribute.values()[0]
attributes.append(attribute)
return {element.tag.replace('-', '_'): attributes}
return element_containers.ElementDict(element_list_type,
{element_list_type:
return_list})
else:
attributes = [xml_to_dict(e, saveroot)
for e in element.getchildren()]
return attributes

return return_list
elif element.get('nil') == 'true':
return None
elif element_type in ('integer', 'datetime', 'date',
Expand Down Expand Up @@ -369,9 +368,11 @@ def xml_to_dict(xmlobj, saveroot=False):
# This is an element with children. The children might be simple
# values, or nested hashes.
if element_type:
attributes = dict(element.items())
attributes = element_containers.ElementDict(
underscore(element.get('type', '')), element.items())
else:
attributes = {}
attributes = element_containers.ElementDict(singularize(
element.tag.replace('-', '_')))
for child in element.getchildren():
attribute = xml_to_dict(child, saveroot)
child_tag = child.tag.replace('-', '_')
Expand All @@ -395,8 +396,8 @@ def xml_to_dict(xmlobj, saveroot=False):
else:
return attributes
elif element.items():
attributes = dict(element.items())
return {element.tag.replace('-', '_'): attributes}
return element_containers.ElementDict(element.tag.replace('-', '_'),
element.items())
else:
return element.text

Expand All @@ -407,4 +408,3 @@ def main():

if __name__ == '__main__':
main()

0 comments on commit 73440f6

Please sign in to comment.