diff --git a/dicttoxml.py b/dicttoxml.py
index 03f1a34..40c115c 100755
--- a/dicttoxml.py
+++ b/dicttoxml.py
@@ -2,7 +2,8 @@
# coding: utf-8
"""
-Converts a native Python dictionary into an XML string. Supports numbers, strings, lists, dictionaries and arbitrary nesting.
+Converts a native Python dictionary into an XML string. Supports numbers,
+strings, lists, dictionaries and arbitrary nesting.
"""
from __future__ import unicode_literals
@@ -14,7 +15,6 @@
import collections
import numbers
import logging
-import sys
from xml.dom.minidom import parseString
@@ -32,35 +32,42 @@
except:
long = int
+
def set_debug(debug=True, filename='dicttoxml.log'):
if debug:
import datetime
print('Debug mode is on. Events are logged at: %s' % (filename))
logging.basicConfig(filename=filename, level=logging.INFO)
- LOG.info('\nLogging session starts: %s' % (str(datetime.datetime.today())))
+ LOG.info('\nLogging session starts: %s' % (
+ str(datetime.datetime.today())))
else:
logging.basicConfig(level=logging.WARNING)
print('Debug mode is off.')
-ids = [] # initialize list of unique ids
+ids = [] # initialize list of unique ids
+
def unicode_me(something):
- """Converts strings with non-ASCII characters to unicode for LOG. Python 3 doesn't have a `unicode()` function, so `unicode()` is an alias for `str()`, but `str()` doesn't take a second argument, hence this kludge."""
+ """Converts strings with non-ASCII characters to unicode for LOG.
+ Python 3 doesn't have a `unicode()` function, so `unicode()` is an
+ alias for `str()`, but `str()` doesn't take a second argument,
+ hence this kludge."""
try:
return unicode(something, 'utf-8')
except:
return unicode(something)
-
+
def make_id(element, start=100000, end=999999):
"""Returns a random integer"""
return '%s_%s' % (element, randint(start, end))
+
def get_unique_id(element):
"""Returns a unique id for a given element"""
this_id = make_id(element)
dup = True
- while dup == True:
+ while dup:
if this_id not in ids:
dup = False
ids.append(this_id)
@@ -68,6 +75,7 @@ def get_unique_id(element):
this_id = make_id(element)
return ids[-1]
+
def get_xml_type(val):
"""Returns the data type for the xml type attribute"""
if type(val).__name__ in ('str', 'unicode'):
@@ -88,9 +96,10 @@ def get_xml_type(val):
return 'list'
return type(val).__name__
+
def xml_escape(s):
if type(s) in (str, unicode):
- s = unicode_me(s) # avoid UnicodeDecodeError
+ s = unicode_me(s) # avoid UnicodeDecodeError
s = s.replace('&', '&')
s = s.replace('"', '"')
s = s.replace('\'', ''')
@@ -98,24 +107,29 @@ def xml_escape(s):
s = s.replace('>', '>')
return s
+
def make_attrstring(attr):
"""Returns an attribute string in the form key="val" """
attrstring = ' '.join(['%s="%s"' % (k, v) for k, v in attr.items()])
return '%s%s' % (' ' if attrstring != '' else '', attrstring)
+
def key_is_valid_xml(key):
"""Checks that a key is a valid XML name"""
LOG.info('Inside key_is_valid_xml(). Testing "%s"' % (unicode_me(key)))
- test_xml = '<%s>foo%s>' % (key, key)
+ test_xml = '<%s>foo%s>' % (
+ key, key)
try:
parseString(test_xml)
return True
- except Exception: #minidom does not implement exceptions well
+ except Exception: # minidom does not implement exceptions well
return False
+
def make_valid_xml_name(key, attr):
"""Tests an XML name and fixes it if invalid"""
- LOG.info('Inside make_valid_xml_name(). Testing key "%s" with attr "%s"' % (unicode_me(key), unicode_me(attr)))
+ LOG.info('Inside make_valid_xml_name(). Testing key "%s" with attr "%s"' %
+ (unicode_me(key), unicode_me(attr)))
# pass through if key is already valid
if key_is_valid_xml(key):
return key, attr
@@ -130,39 +144,47 @@ def make_valid_xml_name(key, attr):
key = 'key'
return key, attr
-def convert(obj, ids, attr_type, parent='root'):
- """Routes the elements of an object to the right function to convert them based on their data type"""
- LOG.info('Inside convert(). obj type is: "%s", obj="%s"' % (type(obj).__name__, unicode_me(obj)))
+
+def convert(obj, ids, attr_type, parent='root', fold_list=True):
+ """Routes the elements of an object to the right function to convert
+ them based on their data type"""
+ LOG.info('Inside convert(). obj type is: "%s", obj="%s"' %
+ (type(obj).__name__, unicode_me(obj)))
if isinstance(obj, numbers.Number) or type(obj) in (str, unicode):
return convert_kv('item', obj, attr_type)
if hasattr(obj, 'isoformat'):
return convert_kv('item', obj.isoformat(), attr_type)
if type(obj) == bool:
return convert_bool('item', obj, attr_type)
- if obj == None:
+ if obj is None:
return convert_none('item', '', attr_type)
if isinstance(obj, dict):
- return convert_dict(obj, ids, parent, attr_type)
+ return convert_dict(obj, ids, parent, attr_type, fold_list)
if isinstance(obj, collections.Iterable):
- return convert_list(obj, ids, parent, attr_type)
- raise TypeError('Unsupported data type: %s (%s)' % (obj, type(obj).__name__))
+ return convert_list(obj, ids, parent, attr_type, fold_list)
+ raise TypeError('Unsupported data type: %s (%s)' %
+ (obj, type(obj).__name__))
+
-def convert_dict(obj, ids, parent, attr_type):
+def convert_dict(obj, ids, parent, attr_type, fold_list):
"""Converts a dict into an XML string."""
- LOG.info('Inside convert_dict(): obj type is: "%s", obj="%s"' % (type(obj).__name__, unicode_me(obj)))
+ LOG.info('Inside convert_dict(): obj type is: "%s", obj="%s"' %
+ (type(obj).__name__, unicode_me(obj)))
output = []
addline = output.append
for key, val in obj.items():
- LOG.info('Looping inside convert_dict(): key="%s", val="%s", type(val)="%s"' % (unicode_me(key), unicode_me(val), type(val).__name__))
+ LOG.info(
+ 'Looping inside convert_dict():key="%s",val="%s",type(val)="%s"' %
+ (unicode_me(key), unicode_me(val), type(val).__name__))
- attr = {} if not ids else {'id': '%s' % (get_unique_id(parent)) }
+ attr = {} if not ids else {'id': '%s' % (get_unique_id(parent))}
key, attr = make_valid_xml_name(key, attr)
if isinstance(val, numbers.Number) or type(val) in (str, unicode):
addline(convert_kv(key, val, attr_type, attr))
- elif hasattr(val, 'isoformat'): # datetime
+ elif hasattr(val, 'isoformat'): # datetime
addline(convert_kv(key, val.isoformat(), attr_type, attr))
elif type(val) == bool:
@@ -172,57 +194,82 @@ def convert_dict(obj, ids, parent, attr_type):
if attr_type:
attr['type'] = get_xml_type(val)
addline('<%s%s>%s%s>' % (
- key, make_attrstring(attr), convert_dict(val, ids, key, attr_type), key)
+ key, make_attrstring(attr),
+ convert_dict(val, ids, key, attr_type, fold_list), key)
)
elif isinstance(val, collections.Iterable):
if attr_type:
attr['type'] = get_xml_type(val)
- addline('<%s%s>%s%s>' % (
- key, make_attrstring(attr), convert_list(val, ids, key, attr_type), key)
- )
+ if fold_list:
+ addline('<%s%s>%s%s>' % (
+ key, make_attrstring(attr),
+ convert_list(val, ids, key, attr_type, fold_list), key)
+ )
+ else:
+ addline(convert_list(val, ids, key, attr_type, fold_list))
elif val is None:
addline(convert_none(key, val, attr_type, attr))
else:
- raise TypeError('Unsupported data type: %s (%s)' % (val, type(val).__name__))
+ raise TypeError('Unsupported data type: %s (%s)' %
+ (val, type(val).__name__))
return ''.join(output)
-def convert_list(items, ids, parent, attr_type):
+
+def convert_list(items, ids, parent, attr_type, fold_list):
"""Converts a list into an XML string."""
LOG.info('Inside convert_list()')
output = []
addline = output.append
+ item_name = 'item' if fold_list else parent
+
if ids:
this_id = get_unique_id(parent)
for i, item in enumerate(items):
- LOG.info('Looping inside convert_list(): item="%s", type="%s"' % (unicode_me(item), type(item).__name__))
- attr = {} if not ids else { 'id': '%s_%s' % (this_id, i+1) }
+ LOG.info('Looping inside convert_list(): item="%s", type="%s"' %
+ (unicode_me(item), type(item).__name__))
+ attr = {} if not ids else {'id': '%s_%s' % (this_id, i+1)}
if isinstance(item, numbers.Number) or type(item) in (str, unicode):
- addline(convert_kv('item', item, attr_type, attr))
- elif hasattr(item, 'isoformat'): # datetime
- addline(convert_kv('item', item.isoformat(), attr_type, attr))
+ addline(convert_kv(item_name, item, attr_type, attr))
+ elif hasattr(item, 'isoformat'): # datetime
+ addline(convert_kv(item_name, item.isoformat(), attr_type, attr))
elif type(item) == bool:
- addline(convert_bool('item', item, attr_type, attr))
+ addline(convert_bool(item_name, item, attr_type, attr))
elif isinstance(item, dict):
if not attr_type:
- addline('- %s
' % (convert_dict(item, ids, parent, attr_type)))
+ addline('<%s>%s%s>' %
+ (item_name,
+ convert_dict(item, ids, parent, attr_type, fold_list),
+ item_name))
else:
- addline('- %s
' % (convert_dict(item, ids, parent, attr_type)))
+ addline('<%s type="dict">%s%s>' % (
+ item_name,
+ convert_dict(item, ids, parent, attr_type, fold_list),
+ item_name))
elif isinstance(item, collections.Iterable):
if not attr_type:
- addline('- %s
' % (make_attrstring(attr), convert_list(item, ids, 'item', attr_type)))
+ addline('<%s %s>%s%s>' % (
+ item_name, make_attrstring(attr),
+ convert_list(item, ids, 'item', attr_type, fold_list),
+ item_name))
else:
- addline('- %s
' % (make_attrstring(attr), convert_list(item, ids, 'item', attr_type)))
+ addline('<%s type="list"%s>%s%s>' % (
+ item_name, make_attrstring(attr),
+ convert_list(item, ids, 'item', attr_type, fold_list),
+ item_name))
elif item is None:
- addline(convert_none('item', None, attr_type, attr))
+ addline(convert_none(item_name, None, attr_type, attr))
else:
- raise TypeError('Unsupported data type: %s (%s)' % (item, type(item).__name__))
+ raise TypeError('Unsupported data type: %s (%s)' %
+ (item, type(item).__name__))
return ''.join(output)
+
def convert_kv(key, val, attr_type, attr={}):
"""Converts a number or string into an XML element"""
- LOG.info('Inside convert_kv(): key="%s", val="%s", type(val) is: "%s"' % (unicode_me(key), unicode_me(val), type(val).__name__))
+ LOG.info('Inside convert_kv(): key="%s", val="%s", type(val) is: "%s"' %
+ (unicode_me(key), unicode_me(val), type(val).__name__))
key, attr = make_valid_xml_name(key, attr)
@@ -233,9 +280,11 @@ def convert_kv(key, val, attr_type, attr={}):
key, attrstring, xml_escape(val), key
)
+
def convert_bool(key, val, attr_type, attr={}):
"""Converts a boolean into an XML element"""
- LOG.info('Inside convert_bool(): key="%s", val="%s", type(val) is: "%s"' % (unicode_me(key), unicode_me(val), type(val).__name__))
+ LOG.info('Inside convert_bool(): key="%s", val="%s", type(val) is: "%s"' %
+ (unicode_me(key), unicode_me(val), type(val).__name__))
key, attr = make_valid_xml_name(key, attr)
@@ -244,6 +293,7 @@ def convert_bool(key, val, attr_type, attr={}):
attrstring = make_attrstring(attr)
return '<%s%s>%s%s>' % (key, attrstring, unicode(val).lower(), key)
+
def convert_none(key, val, attr_type, attr={}):
"""Converts a null value into an XML element"""
LOG.info('Inside convert_none(): key="%s"' % (unicode_me(key)))
@@ -255,18 +305,24 @@ def convert_none(key, val, attr_type, attr={}):
attrstring = make_attrstring(attr)
return '<%s%s>%s>' % (key, attrstring, key)
-def dicttoxml(obj, root=True, custom_root='root', ids=False, attr_type=True):
+
+def dicttoxml(obj, root=True, custom_root='root', ids=False, attr_type=True,
+ fold_list=True):
"""Converts a python object into XML
- attr_type is used to specify if data type for each element should be included in the resulting xml.
+ attr_type is used to specify if data type for each element should be
+ included in the resulting xml.
By default, it is set to True.
"""
- LOG.info('Inside dicttoxml(): type(obj) is: "%s", obj="%s"' % (type(obj).__name__, unicode_me(obj)))
+ LOG.info('Inside dicttoxml(): type(obj) is: "%s", obj="%s"' %
+ (type(obj).__name__, unicode_me(obj)))
output = []
addline = output.append
- if root == True:
+ if root:
addline('')
- addline('<%s>%s%s>' % (custom_root, convert(obj, ids, attr_type, parent=custom_root), custom_root))
+ addline('<%s>%s%s>' % (
+ custom_root,
+ convert(obj, ids, attr_type, custom_root, fold_list),
+ custom_root))
else:
- addline(convert(obj, ids, attr_type, parent=''))
+ addline(convert(obj, ids, attr_type, '', fold_list))
return ''.join(output).encode('utf-8')
-