dict2xml-1.7.7/pytest.ini 0000644 0000000 0000000 00000000121 13615410400 012217 0 ustar 00 [pytest]
addopts = "--tb=short"
testpaths = tests
console_output_style = classic
dict2xml-1.7.7/run.sh 0000755 0000000 0000000 00000002211 13615410400 011333 0 ustar 00 #!/bin/bash
cwd="$(pwd)"
# Bash does not make it easy to find where this file is
# Here I'm making it so it doesn't matter what directory you are in
# when you execute this script. And it doesn't matter whether you're
# executing a symlink to this script
# Note the `-h` in the while loop asks if this path is a symlink
pushd . >'/dev/null'
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
while [ -h "$SCRIPT_PATH" ]; do
cd "$(dirname -- "$SCRIPT_PATH")"
SCRIPT_PATH="$(readlink -f -- "$SCRIPT_PATH")"
done
cd "$(dirname -- "$SCRIPT_PATH")" >'/dev/null'
HANDLED=0
# Special case activate to make the virtualenv active in this session
if [[ "$0" != "$BASH_SOURCE" ]]; then
HANDLED=1
if [[ "activate" == "$1" ]]; then
VENVSTARTER_ONLY_MAKE_VENV=1 ./tools/venv
source ./tools/.python/bin/activate
cd "$cwd"
else
echo "only say \`source run.sh activate\`"
fi
fi
if [[ $HANDLED != 1 ]]; then
if [[ "$#" == "1" && "$1" == "activate" ]]; then
if [[ "$0" = "$BASH_SOURCE" ]]; then
echo "You need to run as 'source ./run.sh $1'"
exit 1
fi
fi
exec ./tools/venv "$@"
fi
dict2xml-1.7.7/test.sh 0000755 0000000 0000000 00000000201 13615410400 011503 0 ustar 00 #!/bin/bash
set -e
export TESTS_CHDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd $TESTS_CHDIR
./tools/venv tests -q $@
dict2xml-1.7.7/dict2xml/__init__.py 0000644 0000000 0000000 00000001120 13615410400 014025 0 ustar 00 from dict2xml.logic import Converter, DataSorter, Node
from .version import VERSION
def dict2xml(
data,
wrap=None,
indent=" ",
newlines=True,
iterables_repeat_wrap=True,
closed_tags_for=None,
data_sorter=None,
):
"""Return an XML string of a Python dict object."""
return Converter(wrap=wrap, indent=indent, newlines=newlines).build(
data,
iterables_repeat_wrap=iterables_repeat_wrap,
closed_tags_for=closed_tags_for,
data_sorter=data_sorter,
)
__all__ = ["dict2xml", "Converter", "Node", "VERSION", "DataSorter"]
dict2xml-1.7.7/dict2xml/logic.py 0000644 0000000 0000000 00000023544 13615410400 013401 0 ustar 00 import collections
import collections.abc
import re
start_ranges = "|".join(
"[{0}]".format(r)
for r in [
"\xc0-\xd6",
"\xd8-\xf6",
"\xf8-\u02ff",
"\u0370-\u037d",
"\u037f-\u1fff",
"\u200c-\u200d",
"\u2070-\u218f",
"\u2c00-\u2fef",
"\u3001-\ud7ff",
"\uf900-\ufdcf",
"\ufdf0-\ufffd",
]
)
NameStartChar = re.compile(r"(:|[A-Z]|_|[a-z]|{0})".format(start_ranges))
NameChar = re.compile(r"(\-|\.|[0-9]|\xB7|[\u0300-\u036F]|[\u203F-\u2040])")
########################
### NODE
########################
class DataSorter:
"""
Used to sort a map of data depending on it's type
"""
def keys_from(self, data):
sorted_data = data
if not isinstance(data, collections.OrderedDict):
sorted_data = sorted(data)
return sorted_data
class always:
def keys_from(self, data):
return sorted(data)
class never:
def keys_from(self, data):
return data
class Node(object):
"""
Represents each tag in the tree
Each node has _either_ a single value or one or more children
If it has a value:
The serialized result is <%(tag)s>%(value)s%(tag)s>
If it has children:
The serialized result is
<%(wrap)s>
%(children)s
%(wrap)s>
Which one it is depends on the implementation of self.convert
"""
# A mapping of characters to treat as escapable entities and their replacements
entities = [("&", "&"), ("<", "<"), (">", ">")]
def __init__(
self,
wrap="",
tag="",
data=None,
iterables_repeat_wrap=True,
closed_tags_for=None,
data_sorter=None,
):
self.tag = self.sanitize_element(tag)
self.wrap = self.sanitize_element(wrap)
self.data = data
self.type = self.determine_type()
self.data_sorter = data_sorter if data_sorter is not None else DataSorter()
self.closed_tags_for = closed_tags_for
self.iterables_repeat_wrap = iterables_repeat_wrap
if self.type == "flat" and isinstance(self.data, str):
# Make sure we deal with entities
for entity, replacement in self.entities:
self.data = self.data.replace(entity, replacement)
def serialize(self, indenter):
"""Returns the Node serialized as an xml string"""
# Determine the start and end of this node
wrap = self.wrap
end, start = "", ""
if wrap:
end = "{0}>".format(wrap)
start = "<{0}>".format(wrap)
if self.closed_tags_for and self.data in self.closed_tags_for:
return "<{0}/>".format(self.wrap)
# Convert the data attached in this node into a value and children
value, children = self.convert()
# Determine the content of the node (essentially the children as a string value)
content = ""
if children:
if self.type != "iterable":
# Non-iterable wraps all it's children in the same tag
content = indenter((c.serialize(indenter) for c in children), wrap)
else:
if self.iterables_repeat_wrap:
# Iterables repeat the wrap for each child
result = []
for c in children:
content = c.serialize(indenter)
if c.type == "flat":
# Child with value, it already is surrounded by the tag
result.append(content)
else:
# Child with children of it's own, they need to be wrapped by start and end
content = indenter([content], True)
result.append("".join((start, content, end)))
# We already have what we want, return the indented result
return indenter(result, False)
else:
result = []
for c in children:
result.append(c.serialize(indenter))
return "".join([start, indenter(result, True), end])
# If here, either:
# * Have a value
# * Or this node is not an iterable
return "".join((start, value, content, end))
def determine_type(self):
"""
Return the type of the data on this node as an identifying string
* Iterable : Supports "for item in data"
* Mapping : Supports "for key in data: value = data[key]"
* flat : A string or something that isn't iterable or a mapping
"""
data = self.data
if isinstance(data, str):
return "flat"
elif isinstance(data, collections.abc.Mapping):
return "mapping"
elif isinstance(data, collections.abc.Iterable):
return "iterable"
else:
return "flat"
def convert(self):
"""
Convert data on this node into a (value, children) tuple depending on the type of the data
If the type is :
* flat : Use self.tag to surround the value. value
* mapping : Return a list of tags where the key for each child is the wrap for that node
* iterable : Return a list of Nodes where self.wrap is the tag for that node
"""
val = ""
typ = self.type
data = self.data
children = []
if typ == "mapping":
sorted_keys = self.data_sorter.keys_from(data)
for key in sorted_keys:
item = data[key]
children.append(
Node(
key,
"",
item,
iterables_repeat_wrap=self.iterables_repeat_wrap,
closed_tags_for=self.closed_tags_for,
data_sorter=self.data_sorter,
)
)
elif typ == "iterable":
for item in data:
children.append(
Node(
"",
self.wrap,
item,
iterables_repeat_wrap=self.iterables_repeat_wrap,
closed_tags_for=self.closed_tags_for,
data_sorter=self.data_sorter,
)
)
else:
val = str(data)
if self.tag:
val = "<{0}>{1}{2}>".format(self.tag, val, self.tag)
return val, children
@staticmethod
def sanitize_element(wrap):
"""
Convert `wrap` into a valid tag name applying the XML Naming Rules.
* Names can contain letters, numbers, and other characters
* Names cannot start with a number or punctuation character
* Names cannot start with the letters xml (or XML, or Xml, etc)
* Names cannot contain spaces
* Any name can be used, no words are reserved.
:ref: http://www.w3.org/TR/REC-xml/#NT-NameChar
"""
if wrap and isinstance(wrap, str):
if wrap.lower().startswith("xml"):
wrap = "_" + wrap
return "".join(
["_" if not NameStartChar.match(wrap) else ""]
+ ["_" if not (NameStartChar.match(c) or NameChar.match(c)) else c for c in wrap]
)
else:
return wrap
########################
### CONVERTER
########################
class Converter(object):
"""Logic for creating a Node tree and serialising that tree into a string"""
def __init__(self, wrap=None, indent=" ", newlines=True):
"""
wrap: The tag that the everything else will be contained within
indent: The string that is multiplied at the start of each new line, to represent each level of nesting
newlines: A boolean specifying whether we want each tag on a new line.
Note that indent only works if newlines is True
"""
self.wrap = wrap
self.indent = indent
self.newlines = newlines
def _make_indenter(self):
"""Returns a function that given a list of strings, will return that list as a single, indented, string"""
indent = self.indent
newlines = self.newlines
if not newlines:
# No newlines, don't care about indentation
ret = lambda nodes, wrapped: "".join(nodes)
else:
if not indent:
indent = ""
def eachline(nodes):
"""Yield each line in each node"""
for node in nodes:
for line in node.split("\n"):
yield line
def ret(nodes, wrapped):
"""
Indent nodes depending on value of wrapped and indent
If not wrapped, then don't indent
Otherwise,
Seperate each child by a newline
and indent each line in the child by one indent unit
"""
if wrapped:
seperator = "\n{0}".format(indent)
surrounding = "\n{0}{{0}}\n".format(indent)
else:
seperator = "\n"
surrounding = "{0}"
return surrounding.format(seperator.join(eachline(nodes)))
return ret
def build(self, data, iterables_repeat_wrap=True, closed_tags_for=None, data_sorter=None):
"""Create a Node tree from the data and return it as a serialized xml string"""
indenter = self._make_indenter()
return Node(
wrap=self.wrap,
data=data,
iterables_repeat_wrap=iterables_repeat_wrap,
closed_tags_for=closed_tags_for,
data_sorter=data_sorter,
).serialize(indenter)
dict2xml-1.7.7/dict2xml/version.py 0000644 0000000 0000000 00000000022 13615410400 013753 0 ustar 00 VERSION = "1.7.7"
dict2xml-1.7.7/tests/__init__.py 0000644 0000000 0000000 00000000000 13615410400 013435 0 ustar 00 dict2xml-1.7.7/tests/build_test.py 0000644 0000000 0000000 00000007347 13615410400 014061 0 ustar 00 import json
import os
from textwrap import dedent
from unittest import mock
import pytest
from dict2xml import Converter, DataSorter, dict2xml
examples = os.path.join(os.path.dirname(__file__), "examples")
class TestBuild:
class TestConvenienceFunction:
def test_it_creates_a_converter_with_args_and_kwargs_and_calls_build_on_it_with_provided_data(
self,
):
data = mock.Mock(name="data")
serialized = mock.Mock(name="serialized")
converter = mock.Mock(name="converter")
converter.build.return_value = serialized
FakeConverter = mock.Mock(name="Converter", return_value=converter)
data_sorter = DataSorter.never()
with mock.patch("dict2xml.Converter", FakeConverter):
assert (
dict2xml(
data,
wrap="wrap",
indent="indent",
newlines=False,
iterables_repeat_wrap=False,
closed_tags_for=["one"],
data_sorter=data_sorter,
)
is serialized
)
FakeConverter.assert_called_once_with(wrap="wrap", indent="indent", newlines=False)
converter.build.assert_called_once_with(
data,
iterables_repeat_wrap=False,
closed_tags_for=["one"],
data_sorter=data_sorter,
)
class TestJustWorking:
@pytest.fixture()
def assertResult(self):
def assertResult(result, **kwargs):
data = {"a": [1, 2, 3], "b": {"c": "d", "e": {"f": "g"}}, "d": 1}
converter = Converter(wrap="all", **kwargs)
print(converter.build(data))
assert dedent(result).strip() == converter.build(data)
return assertResult
def test_with_both_indentation_and_newlines(self, assertResult):
expected = """
1
2
3
d
g
1
"""
assertResult(expected, indent=" ", newlines=True)
def test_with_just_newlines(self, assertResult):
expected = """
1
2
3
d
g
1
"""
assertResult(expected, indent=None, newlines=True)
def test_with_just_indentation(self, assertResult):
# Indentation requires newlines to work
expected = "123dg1"
assertResult(expected, indent=" ", newlines=False)
def test_with_no_whitespace(self, assertResult):
expected = "123dg1"
assertResult(expected, indent=None, newlines=False)
def test_works_on_a_massive_complex_dictionary(self):
with open(os.path.join(examples, "python_dict.json"), "r") as fle:
data = json.load(fle)
with open(os.path.join(examples, "result.xml"), "r") as fle:
result = fle.read()
converter = Converter(wrap="all", indent=" ", newlines=True)
assert dedent(result).strip() == converter.build(data)
dict2xml-1.7.7/tests/converter_test.py 0000644 0000000 0000000 00000017250 13615410400 014763 0 ustar 00 from textwrap import dedent
from unittest import mock
import pytest
from dict2xml import Converter
class TestConverter:
class TestBuilding:
def test_creates_an_indenter_a_node_and_then_calls_serialize_on_the_node_with_the_indenter(
self,
):
wrap = mock.Mock("wrap")
indent = mock.Mock("indent")
newlines = mock.Mock("newlines")
converter = Converter(wrap, indent, newlines)
node = mock.Mock(name="node")
FakeNode = mock.Mock(name="Node", return_value=node)
serialized = mock.Mock(name="serialized")
node.serialize.return_value = serialized
indenter = mock.Mock(name="indenter")
make_indenter = mock.Mock(name="make_indenter", return_value=indenter)
mip = mock.patch.object(converter, "_make_indenter", make_indenter)
fnp = mock.patch("dict2xml.logic.Node", FakeNode)
data = mock.Mock(name="data")
with mip, fnp:
assert converter.build(data) is serialized
FakeNode.assert_called_once_with(
wrap=wrap,
data=data,
iterables_repeat_wrap=True,
closed_tags_for=None,
data_sorter=None,
)
node.serialize.assert_called_once_with(indenter)
def tes_does_not_repeat_the_wrap_of_iterables_repeat_wrap_is_false(self):
example = {
"array": [
{"item": {"string1": "string", "string2": "string"}},
{"item": {"string1": "other string", "string2": "other string"}},
]
}
result = Converter("").build(example, iterables_repeat_wrap=False)
assert (
result
== dedent(
"""
-
string
string
-
other string
other string
"""
).strip()
)
def test_can_produce_self_closing_tags(self):
example = {
"item1": None,
"item2": {"string1": "", "string2": None},
"item3": "special",
}
result = Converter("").build(example, closed_tags_for=[None])
assert (
result
== dedent(
"""
special
"""
).strip()
)
result = Converter("").build(example, closed_tags_for=[None, ""])
assert (
result
== dedent(
"""
special
"""
).strip()
)
result = Converter("").build(example, closed_tags_for=["special"])
print(result)
assert (
result
== dedent(
"""
None
None
"""
).strip()
)
class TestMakingIndentationFunction:
@pytest.fixture()
def V(self):
class V:
with_indent = Converter(indent=" ", newlines=True)
without_indent = Converter(indent="", newlines=True)
without_newlines = Converter(newlines=False)
def assertIndenter(self, indenter, nodes, wrap, expected):
result = "".join([wrap, indenter(nodes, wrap), wrap])
assert result == expected.strip()
return V()
class TestNoNewlines:
def test_joins_nodes_with_empty_string(self, V):
indenter = V.without_newlines._make_indenter()
assert indenter(["a", "b", "c"], True) == "abc"
assert indenter(["d", "e", "f"], False) == "def"
class TestWithNewlines:
class TestNoIndentation:
def test_joins_with_newlines_and_never_indents(self, V):
# Wrap is added to expected output via test_indenter
indenter = V.without_indent._make_indenter()
V.assertIndenter(
indenter,
["a", "b", "c"],
"<>",
dedent(
"""
<>
a
b
c
<>"""
),
)
class TestWithIndentation:
def test_joins_with_newlines_and_indents_if_there_is_a_wrapping_tag(self, V):
# Wrap is added to expected output via test_indenter
indenter = V.with_indent._make_indenter()
V.assertIndenter(
indenter,
["a", "b", "c"],
"<>",
dedent(
"""
<>
a
b
c
<>"""
),
)
def test_joins_with_newlines_but_does_not_indent_if_no_wrapping_tag(self, V):
indenter = V.with_indent._make_indenter()
V.assertIndenter(
indenter,
["a", "b", "c"],
"",
dedent(
"""
a
b
c"""
),
)
def test_it_reindents_each_new_line(self, V):
node1 = dedent(
"""
a
b
c
d
e
"""
).strip()
node2 = "f"
node3 = dedent(
"""
f
g
h
"""
).strip()
# Wrap is added to expected output via test_indenter
indenter = V.with_indent._make_indenter()
V.assertIndenter(
indenter,
[node1, node2, node3],
"<>",
dedent(
"""
<>
a
b
c
d
e
f
f
g
h
<>
"""
),
)
dict2xml-1.7.7/tests/node_test.py 0000644 0000000 0000000 00000026452 13615410400 013705 0 ustar 00 import collections
import collections.abc
from unittest import mock
from dict2xml import DataSorter, Node
class TestNode:
def test_determines_type_at_instantiation(self):
assert Node(data={}).type == "mapping"
assert Node(data=[]).type == "iterable"
for d in ["", "asdf", "", "asdf", 0, 1, False, True]:
assert Node(data=d).type == "flat"
class TestHandlingEntities:
def test_will_change_string_data_to_take_entities_into_account(self):
node = Node(data="<2&a>")
assert node.data == "<2&a>"
class TestDetermininType:
def assertType(self, *datas, **kwargs):
expected = kwargs.get("expected", None)
for d in datas:
assert Node(data=d).determine_type() == expected
def test_says_strings_are_falt(self):
self.assertType("", "asdf", "", "asdf", expected="flat")
def test_says_numbers_and_booleans_are_flat(self):
self.assertType(0, 1, False, True, expected="flat")
def test_says_anything_that_implements_dunder_iter_is_an_iterable(self):
class IterableObject(object):
def __iter__(s):
return []
self.assertType((), [], set(), IterableObject(), expected="iterable")
def test_says_anything_that_is_a_dict_or_subclass_of_collections_Mapping_is_a_mapping(
self,
):
class MappingObject(collections.abc.Mapping):
def __len__(s):
return 0
def __iter__(s):
return []
def __getitem__(s, key):
return key
self.assertType({}, MappingObject(), expected="mapping")
def test_can_not_determine_if_an_object_is_a_mapping_if_it_is_not_subclass_of_collections_Mapping(
self,
):
# Would be great if possible, but doesn't seem to be :(
class WantsToBeMappingObject(object):
def __iter__(s):
return []
def __getitem__(s, key):
return key
self.assertType(WantsToBeMappingObject(), expected="iterable")
class TestConversion:
def test_it_returns_list_of_Nodes_with_key_as_wrap_and_item_as_data_if_type_is_mapping(
self,
):
called = []
nodes = [mock.Mock(name="n{0}".format(i)) for i in range(3)]
def N(*args, **kwargs):
called.append(1)
return nodes[len(called) - 1]
ds = DataSorter()
irw = mock.Mock("irw")
ctf = mock.Mock("ctf")
FakeNode = mock.Mock(name="Node", side_effect=N)
with mock.patch("dict2xml.logic.Node", FakeNode):
data = dict(a=1, b=2, c=3)
result = Node(
data=data,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
).convert()
assert result == ("", nodes)
assert FakeNode.mock_calls == [
mock.call(
"a",
"",
1,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
mock.call(
"b",
"",
2,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
mock.call(
"c",
"",
3,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
]
def test_it_respects_the_order_of_an_ordered_dict(self):
called = []
nodes = [mock.Mock(name="n{0}".format(i)) for i in range(3)]
def N(*args, **kwargs):
called.append(1)
return nodes[len(called) - 1]
ds = DataSorter()
irw = mock.Mock("irw")
ctf = mock.Mock("ctf")
FakeNode = mock.Mock(name="Node", side_effect=N)
with mock.patch("dict2xml.logic.Node", FakeNode):
data = collections.OrderedDict([("b", 2), ("c", 3), ("a", 1)])
result = Node(
data=data,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
).convert()
assert result == ("", nodes)
assert FakeNode.mock_calls == [
mock.call(
"b",
"",
2,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
mock.call(
"c",
"",
3,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
mock.call(
"a",
"",
1,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
]
def test_it_can_be_told_to_also_sort_OrderdDict(self):
called = []
nodes = [mock.Mock(name="n{0}".format(i)) for i in range(3)]
def N(*args, **kwargs):
called.append(1)
return nodes[len(called) - 1]
ds = DataSorter.always()
irw = mock.Mock("irw")
ctf = mock.Mock("ctf")
FakeNode = mock.Mock(name="Node", side_effect=N)
with mock.patch("dict2xml.logic.Node", FakeNode):
data = collections.OrderedDict([("b", 2), ("c", 3), ("a", 1)])
result = Node(
data=data,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
).convert()
assert result == ("", nodes)
assert FakeNode.mock_calls == [
mock.call(
"a",
"",
1,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
mock.call(
"b",
"",
2,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
mock.call(
"c",
"",
3,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
]
def test_it_can_be_told_to_never_sort(self):
called = []
nodes = [mock.Mock(name="n{0}".format(i)) for i in range(3)]
def N(*args, **kwargs):
called.append(1)
return nodes[len(called) - 1]
ds = DataSorter.never()
irw = mock.Mock("irw")
ctf = mock.Mock("ctf")
FakeNode = mock.Mock(name="Node", side_effect=N)
with mock.patch("dict2xml.logic.Node", FakeNode):
data = {"c": 3, "a": 1, "b": 2}
result = Node(
data=data,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
).convert()
assert result == ("", nodes)
assert FakeNode.mock_calls == [
mock.call(
"c",
"",
3,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
mock.call(
"a",
"",
1,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
mock.call(
"b",
"",
2,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
]
def test_it_returns_list_of_Nodes_with_wrap_as_tag_and_item_as_data_if_type_is_iterable(
self,
):
called = []
nodes = [mock.Mock(name="n{0}".format(i)) for i in range(3)]
def N(*args, **kwargs):
called.append(1)
return nodes[len(called) - 1]
ds = DataSorter()
irw = mock.Mock("irw")
ctf = mock.Mock("ctf")
FakeNode = mock.Mock(name="Node", side_effect=N)
with mock.patch("dict2xml.logic.Node", FakeNode):
data = [1, 2, 3]
result = Node(
data=data,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
).convert()
assert result == ("", nodes)
assert FakeNode.mock_calls == [
mock.call(
"",
"",
1,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
mock.call(
"",
"",
2,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
mock.call(
"",
"",
3,
iterables_repeat_wrap=irw,
closed_tags_for=ctf,
data_sorter=ds,
),
]
def test_it_returns_data_enclosed_in_tags_made_from_self_tag_if_not_iterable_or_mapping(
self,
):
tag = "thing"
results = []
for d in [0, 1, "", "", "asdf", "qwer", False, True]:
val, children = Node(tag=tag, data=d).convert()
assert len(children) == 0
results.append(val)
assert results == [
"0",
"1",
"",
"",
"asdf",
"qwer",
"False",
"True",
]
def test_it_returns_data_as_is_if_not_iterable_or_mapping_and_no_self_tag(self):
tag = ""
results = []
for d in [0, 1, "", "", "asdf", "qwer", False, True]:
val, children = Node(tag=tag, data=d).convert()
assert len(children) == 0
results.append(val)
assert results == ["0", "1", "", "", "asdf", "qwer", "False", "True"]
dict2xml-1.7.7/tests/examples/__init__.py 0000644 0000000 0000000 00000000000 13615410400 015253 0 ustar 00 dict2xml-1.7.7/tests/examples/python_dict.json 0000644 0000000 0000000 00000007540 13615410400 016401 0 ustar 00 {
"web-app": {
"servlet": [
{
"servlet-name": "cofaxCDS",
"servlet-class": "org.cofax.cds.CDSServlet",
"init-param": {
"configGlossary:installationAt": "Philadelphia PA",
"configGlossary:adminEmail": "ksm@pobox.com",
"configGlossary:poweredBy": "Cofax",
"configGlossary:poweredByIcon": "/images/cofax.gif",
"configGlossary:staticPath": "/content/static",
"templateProcessorClass": "org.cofax.WysiwygTemplate",
"templateLoaderClass": "org.cofax.FilesTemplateLoader",
"templatePath": "templates",
"templateOverridePath": "",
"defaultListTemplate": "listTemplate.htm",
"defaultFileTemplate": "articleTemplate.htm",
"useJSP": false,
"jspListTemplate": "listTemplate.jsp",
"jspFileTemplate": "articleTemplate.jsp",
"cachePackageTagsTrack": 200,
"cachePackageTagsStore": 200,
"cachePackageTagsRefresh": 60,
"cacheTemplatesTrack": 100,
"cacheTemplatesStore": 50,
"cacheTemplatesRefresh": 15,
"cachePagesTrack": 200,
"cachePagesStore": 100,
"cachePagesRefresh": 10,
"cachePagesDirtyRead": 10,
"searchEngineListTemplate": "forSearchEnginesList.htm",
"searchEngineFileTemplate": "forSearchEngines.htm",
"searchEngineRobotsDb": "WEB-INF/robots.db",
"useDataStore": true,
"dataStoreClass": "org.cofax.SqlDataStore",
"redirectionClass": "org.cofax.SqlRedirection",
"dataStoreName": "cofax",
"dataStoreDriver": "com.microsoft.jdbc.sqlserver.SQLServerDriver",
"dataStoreUrl": "jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon",
"dataStoreUser": "sa",
"dataStorePassword": "dataStoreTestQuery",
"dataStoreTestQuery": "SET NOCOUNT ON;select test='test';",
"dataStoreLogFile": "/usr/local/tomcat/logs/datastore.log",
"dataStoreInitConns": 10,
"dataStoreMaxConns": 100,
"dataStoreConnUsageLimit": 100,
"dataStoreLogLevel": "debug",
"maxUrlLength": 500
}
},
{
"servlet-name": "cofaxEmail",
"servlet-class": "org.cofax.cds.EmailServlet",
"init-param": {
"mailHost": "mail1",
"mailHostOverride": "mail2"
}
},
{
"servlet-name": "cofaxAdmin",
"servlet-class": "org.cofax.cds.AdminServlet"
},
{
"servlet-name": "fileServlet",
"servlet-class": "org.cofax.cds.FileServlet"
},
{
"servlet-name": "cofaxTools",
"servlet-class": "org.cofax.cms.CofaxToolsServlet",
"init-param": {
"templatePath": "toolstemplates/",
"log": 1,
"logLocation": "/usr/local/tomcat/logs/CofaxTools.log",
"logMaxSize": "",
"dataLog": 1,
"dataLogLocation": "/usr/local/tomcat/logs/dataLog.log",
"dataLogMaxSize": "",
"removePageCache": "/content/admin/remove?cache=pages&id=",
"removeTemplateCache": "/content/admin/remove?cache=templates&id=",
"fileTransferFolder": "/usr/local/tomcat/webapps/content/fileTransferFolder",
"lookInContext": 1,
"adminGroupID": 4,
"betaServer": true
}
}
],
"servlet-mapping": {
"cofaxCDS": "/",
"cofaxEmail": "/cofaxutil/aemail/*",
"cofaxAdmin": "/admin/*",
"fileServlet": "/static/*",
"cofaxTools": "/tools/*"
},
"taglib": {
"taglib-uri": "cofax.tld",
"taglib-location": "/WEB-INF/tlds/cofax.tld"
}
},
"lessthan": "<",
"entitylist": [
{
"ampersand": "&"
},
{
"mix": ">p<"
}
],
"3badtagstart": "x",
"xml_is_an_invalid_prefix": "x",
"Xml_with_other_case_is_an_invalid_prefix": "x"
}
dict2xml-1.7.7/tests/examples/result.xml 0000644 0000000 0000000 00000011706 13615410400 015221 0 ustar 00
<_3badtagstart>x
<_Xml_with_other_case_is_an_invalid_prefix>x
&
>p<
<
60
200
200
10
10
100
200
15
50
100
ksm@pobox.com
Philadelphia PA
Cofax
/images/cofax.gif
/content/static
org.cofax.SqlDataStore
100
com.microsoft.jdbc.sqlserver.SQLServerDriver
10
/usr/local/tomcat/logs/datastore.log
debug
100
cofax
dataStoreTestQuery
SET NOCOUNT ON;select test='test';
jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon
sa
articleTemplate.htm
listTemplate.htm
articleTemplate.jsp
listTemplate.jsp
500
org.cofax.SqlRedirection
forSearchEngines.htm
forSearchEnginesList.htm
WEB-INF/robots.db
org.cofax.FilesTemplateLoader
templates
org.cofax.WysiwygTemplate
True
False
org.cofax.cds.CDSServlet
cofaxCDS
mail1
mail2
org.cofax.cds.EmailServlet
cofaxEmail
org.cofax.cds.AdminServlet
cofaxAdmin
org.cofax.cds.FileServlet
fileServlet
4
True
1
/usr/local/tomcat/logs/dataLog.log
/usr/local/tomcat/webapps/content/fileTransferFolder
1
/usr/local/tomcat/logs/CofaxTools.log
1
/content/admin/remove?cache=pages&id=
/content/admin/remove?cache=templates&id=
toolstemplates/
org.cofax.cms.CofaxToolsServlet
cofaxTools
/admin/*
/
/cofaxutil/aemail/*
/tools/*
/static/*
/WEB-INF/tlds/cofax.tld
cofax.tld
<_xml_is_an_invalid_prefix>x
dict2xml-1.7.7/tools/bootstrap_venvstarter.py 0000644 0000000 0000000 00000001631 13615410400 016367 0 ustar 00 import os
import runpy
import sys
from pathlib import Path
deps_dir = Path(__file__).parent / "deps"
if not deps_dir.exists():
deps_dir.mkdir()
if not (deps_dir / "venvstarter.py").exists():
if "PIP_REQUIRE_VIRTUALENV" in os.environ:
del os.environ["PIP_REQUIRE_VIRTUALENV"]
os.system(f"{sys.executable} -m pip install venvstarter -t {deps_dir}")
sys.path.append(str(deps_dir))
venvstarter_module = runpy.run_path(str(deps_dir / "venvstarter.py"))
sys.path.pop()
wanted_version = "0.12.2"
upgrade = False
VERSION = venvstarter_module.get("VERSION")
if VERSION is None:
upgrade = True
else:
Version = venvstarter_module["Version"]
if Version(VERSION) != Version(wanted_version):
upgrade = True
if upgrade:
os.system(f"{sys.executable} -m pip install -U 'venvstarter=={wanted_version}' -t {deps_dir}")
manager = runpy.run_path(str(deps_dir / "venvstarter.py"))["manager"]
dict2xml-1.7.7/tools/devtools.py 0000644 0000000 0000000 00000006323 13615410400 013551 0 ustar 00 import inspect
import os
import platform
import shlex
import sys
import typing as tp
from pathlib import Path
here = Path(__file__).parent
if platform.system() == "Windows":
import mslex # type:ignore[import]
shlex = mslex # noqa
if sys.version_info < (3, 10):
Dict = tp.Dict
List = tp.List
else:
Dict = dict
List = list
class Command:
__is_command__: bool
def __call__(self, bin_dir: Path, args: List[str]) -> None:
pass
def command(func: tp.Callable) -> tp.Callable:
tp.cast(Command, func).__is_command__ = True
return func
def run(*args: tp.Union[str, Path]) -> None:
cmd = " ".join(shlex.quote(str(part)) for part in args)
print(f"Running '{cmd}'")
ret = os.system(cmd)
if ret != 0:
sys.exit(1)
class App:
commands: Dict[str, Command]
def __init__(self):
self.commands = {}
compare = inspect.signature(type("C", (Command,), {})().__call__)
for name in dir(self):
val = getattr(self, name)
if getattr(val, "__is_command__", False):
assert inspect.signature(val) == compare, (
f"Expected '{name}' to have correct signature, have {inspect.signature(val)} instead of {compare}"
)
self.commands[name] = val
def __call__(self, args: List[str]) -> None:
bin_dir = Path(sys.executable).parent
if args and args[0] in self.commands:
os.chdir(here.parent)
self.commands[args[0]](bin_dir, args[1:])
return
sys.exit(
f"Unknown command:\nAvailable: {sorted(self.commands)}\nWanted: {args}"
)
@command
def format(self, bin_dir: Path, args: List[str]) -> None:
if not args:
args = [".", *args]
run(bin_dir / "black", *args)
run(bin_dir / "isort", *args)
@command
def lint(self, bin_dir: Path, args: List[str]) -> None:
run(bin_dir / "pylama", *args)
@command
def tests(self, bin_dir: Path, args: List[str]) -> None:
if "-q" not in args:
args = ["-q", *args]
env = os.environ
files: list[str] = []
if "TESTS_CHDIR" in env:
ags: list[str] = []
test_dir = Path(env["TESTS_CHDIR"]).absolute()
for a in args:
test_name = ""
if "::" in a:
filename, test_name = a.split("::", 1)
else:
filename = a
try:
p = Path(filename).absolute()
except:
ags.append(a)
else:
if p.exists():
rel = p.relative_to(test_dir)
if test_name:
files.append(f"{rel}::{test_name}")
else:
files.append(str(rel))
else:
ags.append(a)
args = ags
os.chdir(test_dir)
run(bin_dir / "pytest", *files, *args)
@command
def tox(self, bin_dir: Path, args: List[str]) -> None:
run(bin_dir / "tox", *args)
app = App()
if __name__ == "__main__":
app(sys.argv[1:])
dict2xml-1.7.7/tools/requirements.dev.txt 0000644 0000000 0000000 00000000411 13615410400 015371 0 ustar 00 types-setuptools==69.1.0.20240309
pylama==8.4.1
neovim==0.3.1
tox==4.17.1
python-lsp-server==1.10.0
python-lsp-black==2.0.0
mslex==1.1.0; sys.platform == 'win32'
isort==5.13.2
pyls-isort==0.2.2
hatch==1.9.3
jedi==0.19.1
setuptools>=69.0.3; python_version >= '3.12'
dict2xml-1.7.7/tools/venv 0000755 0000000 0000000 00000001726 13615410400 012246 0 ustar 00 #!/usr/bin/env python3
import glob
import os
import runpy
import shutil
import subprocess
import sys
import typing as tp
from pathlib import Path
here = Path(__file__).parent
manager = runpy.run_path(str(Path(__file__).parent / "bootstrap_venvstarter.py"))["manager"]
def run(venv_location: Path, args: tp.List[str]) -> tp.Union[None, str, tp.List[str]]:
devtools_location = Path(__file__).parent / "devtools.py"
return ["python", str(devtools_location)]
manager = manager(run).named(".python")
manager.add_local_dep(
"{here}",
"..",
version_file=("dict2xml", "version.py"),
name="dict2xml=={version}",
with_tests=True,
)
if "TOX_PYTHON" in os.environ:
folder = Path(os.environ["TOX_PYTHON"]).parent.parent
manager.place_venv_in(folder.parent)
manager.named(folder.name)
else:
manager.add_no_binary("black")
manager.add_requirements_file("{here}", "requirements.dev.txt")
manager.set_packaging_version('">24"')
manager.run()
dict2xml-1.7.7/.gitignore 0000644 0000000 0000000 00000000056 13615410400 012165 0 ustar 00 *.pyc
build
*.egg-info
dist/
.tox
.dmypy.json
dict2xml-1.7.7/LICENSE 0000644 0000000 0000000 00000002071 13615410400 011201 0 ustar 00 The MIT License (MIT)
Copyright (c) 2018 Stephen Moore
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.
dict2xml-1.7.7/README.rst 0000644 0000000 0000000 00000012271 13615410400 011666 0 ustar 00 dict2xml
========
Super Simple utility to convert a python dictionary into an xml string
Installation
------------
Install using pip::
> python -m pip install dict2xml
example
-------
.. code-block:: python
from dict2xml import dict2xml
data = {
'a': 1,
'b': [2, 3],
'c': {
'd': [
{'p': 9},
{'o': 10}
],
'e': 7
}
}
print dict2xml(data, wrap="all", indent=" ")
Output
------
.. code-block:: xml
1
2
3
9
10
7
methods
-------
``dict2xml.dict2xml(data, *args, **kwargs)``
Equivalent to:
.. code-block:: python
dict2xml.Converter(*args, **kwargs).build(data)
``dict2xml.Converter(wrap="", indent=" ", newlines=True)``
Knows how to convert a dictionary into an xml string
* wrap: Wraps the entire tree in this tag
* indent: Amount to prefix each line for each level of nesting
* newlines: Whether or not to use newlines
``dict2xml.Converter.build(data, iterables_repeat_wrap=True, closed_tags_for=None, data_sorter=None)``
Instance method on Converter that takes in the data and creates the xml string
* iterables_repeat_wrap - when false the key the array is in will be repeated
* closed_tags_for - an array of values that will produce self closing tags
* data_sorter - an object as explained below for sorting keys in maps
``dict2xml.DataSorter``
An object used to determine the sorting of keys for a map of data.
By default an ``OrderedDict`` object will not have it's keys sorted, but any
other type of mapping will.
It can be made so even ``OrderedDict`` will get sorted by passing in
``data_sorter=DataSorter.always()``.
Or it can be made so that keys are produced from the sorting determined by
the mapping with ``data_sorter=DataSorter.never()``.
.. note:: When this library was first created python did not have deterministic
sorting for normal dictionaries which is why default everything gets sorted but
``OrderedDict`` do not.
To create custom sorting logic requires an object that has a single ``keys_from``
method on it that accepts a map of data and returns a list of strings, where only
the keys that appear in the list will go into the output and those keys must exist
in the original mapping.
Self closing tags
-----------------
To produce self closing tags (like `` ``) then the ``build`` method must
be given a list of values under ``closed_tags_for``. For example, if you want
``None`` to produce a closing tag then:
.. code-block:: python
example = {
"item1": None,
"item2": {"string1": "", "string2": None},
"item3": "special",
}
result = Converter("").build(example, closed_tags_for=[None])
assert result == dedent("""
special
""").strip())
Here only ``string2`` gets a self closing tag because it has data of ``None``,
which has been designated as special.
If you want to dynamically work out which tags should be self closing then you
may provide an object that implements ``__eq__`` and do your logic there.
Limitations
-----------
* No attributes on elements
* Currently no explicit way to hook into how to cope with your custom data
* Currently no way to insert an xml declaration line
Changelog
---------
1.7.7 - 10 July 2025
* Converted the tests to plain python to remove the noseOfYeti dependency
1.7.6 - 8 August 2024
* Fixed the ``dict2xml.dict2xml`` entry point to distribute options
correctly
1.7.5 - 13 February 2024
* Introduced the ``data_sorter`` option
1.7.4 - 16 January 2024
* Make the tests compatible with pytest8
1.7.3 - 25 Feb 2023
* This version has no changes to the installed code.
* This release converts to hatch for packaging and adds a wheel to the
package on pypi.
* CI will now run against python 3.11 as well
1.7.2 - 18 Oct 2022
* This version has no changes to the installed code.
* This release adds the tests to the source distribution put onto pypi.
1.7.1 - 16 Feb 2022
* Adding an option to have self closing tags when the value for that
tag equals certain values
1.7.0 - 16 April, 2020
* Use collections.abc to avoid deprecation warning. Thanks @mangin.
* This library no longer supports Python2 and is only supported for
Python3.6+. Note that the library should still work in Python3.5 as I
have not used f-strings, but the framework I use for the tests is only 3.6+.
1.6.1 - August 27, 2019
* Include readme and LICENSE in the package
1.6 - April 27, 2018
* No code changes
* changed the licence to MIT
* Added more metadata to pypi
* Enabled travis ci
* Updated the tests slightly
1.5
* No changelog was kept before this point.
Development
-----------
To enter a virtualenv with dict2xml and dev requirements installed run::
> source run.sh activate
Tests may be run with::
> ./test.sh
Or::
> ./run.sh tox
Linting and formatting is via::
> ./format
> ./lint
dict2xml-1.7.7/pyproject.toml 0000644 0000000 0000000 00000002662 13615410400 013116 0 ustar 00 [build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "dict2xml"
dynamic = ["version"]
description = "Small utility to convert a python dictionary into an XML string"
readme = "README.rst"
license = "MIT"
requires-python = ">= 3.5"
authors = [
{ name = "Stephen Moore", email = "stephen@delfick.com" },
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Text Processing :: Markup :: XML",
]
[project.optional-dependencies]
tests = [
"pytest==8.3.2",
]
[project.urls]
Homepage = "http://github.com/delfick/python-dict2xml"
[tool.hatch.version]
path = "dict2xml/version.py"
[tool.hatch.build.targets.sdist]
include = [
"/dict2xml",
"/README.rst",
"/LICENSE",
"/test.sh",
"/run.sh",
"/pytest.ini",
"/tests/**",
"/tools/bootstrap_venvstarter.py",
"/tools/requirements.dev.txt",
"/tools/devtools.py",
"/tools/venv"
]
exclude = ["*.pyc"]
[tool.hatch.build]
include = ["/dict2xml"]
[tool.black]
line-length = 100
include = '\.py$'
exclude = '''
/(
\.git
| \.tox
| dist
| tools
)/
'''
[tool.isort]
profile = "black"
skip_glob = [
".git/*",
".tox/*",
"dist/*",
"tools/.*",
]
dict2xml-1.7.7/PKG-INFO 0000644 0000000 0000000 00000013715 13615410400 011300 0 ustar 00 Metadata-Version: 2.4
Name: dict2xml
Version: 1.7.7
Summary: Small utility to convert a python dictionary into an XML string
Project-URL: Homepage, http://github.com/delfick/python-dict2xml
Author-email: Stephen Moore
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Markup :: XML
Requires-Python: >=3.5
Provides-Extra: tests
Requires-Dist: pytest==8.3.2; extra == 'tests'
Description-Content-Type: text/x-rst
dict2xml
========
Super Simple utility to convert a python dictionary into an xml string
Installation
------------
Install using pip::
> python -m pip install dict2xml
example
-------
.. code-block:: python
from dict2xml import dict2xml
data = {
'a': 1,
'b': [2, 3],
'c': {
'd': [
{'p': 9},
{'o': 10}
],
'e': 7
}
}
print dict2xml(data, wrap="all", indent=" ")
Output
------
.. code-block:: xml
1
2
3
9
10
7
methods
-------
``dict2xml.dict2xml(data, *args, **kwargs)``
Equivalent to:
.. code-block:: python
dict2xml.Converter(*args, **kwargs).build(data)
``dict2xml.Converter(wrap="", indent=" ", newlines=True)``
Knows how to convert a dictionary into an xml string
* wrap: Wraps the entire tree in this tag
* indent: Amount to prefix each line for each level of nesting
* newlines: Whether or not to use newlines
``dict2xml.Converter.build(data, iterables_repeat_wrap=True, closed_tags_for=None, data_sorter=None)``
Instance method on Converter that takes in the data and creates the xml string
* iterables_repeat_wrap - when false the key the array is in will be repeated
* closed_tags_for - an array of values that will produce self closing tags
* data_sorter - an object as explained below for sorting keys in maps
``dict2xml.DataSorter``
An object used to determine the sorting of keys for a map of data.
By default an ``OrderedDict`` object will not have it's keys sorted, but any
other type of mapping will.
It can be made so even ``OrderedDict`` will get sorted by passing in
``data_sorter=DataSorter.always()``.
Or it can be made so that keys are produced from the sorting determined by
the mapping with ``data_sorter=DataSorter.never()``.
.. note:: When this library was first created python did not have deterministic
sorting for normal dictionaries which is why default everything gets sorted but
``OrderedDict`` do not.
To create custom sorting logic requires an object that has a single ``keys_from``
method on it that accepts a map of data and returns a list of strings, where only
the keys that appear in the list will go into the output and those keys must exist
in the original mapping.
Self closing tags
-----------------
To produce self closing tags (like `` ``) then the ``build`` method must
be given a list of values under ``closed_tags_for``. For example, if you want
``None`` to produce a closing tag then:
.. code-block:: python
example = {
"item1": None,
"item2": {"string1": "", "string2": None},
"item3": "special",
}
result = Converter("").build(example, closed_tags_for=[None])
assert result == dedent("""
special
""").strip())
Here only ``string2`` gets a self closing tag because it has data of ``None``,
which has been designated as special.
If you want to dynamically work out which tags should be self closing then you
may provide an object that implements ``__eq__`` and do your logic there.
Limitations
-----------
* No attributes on elements
* Currently no explicit way to hook into how to cope with your custom data
* Currently no way to insert an xml declaration line
Changelog
---------
1.7.7 - 10 July 2025
* Converted the tests to plain python to remove the noseOfYeti dependency
1.7.6 - 8 August 2024
* Fixed the ``dict2xml.dict2xml`` entry point to distribute options
correctly
1.7.5 - 13 February 2024
* Introduced the ``data_sorter`` option
1.7.4 - 16 January 2024
* Make the tests compatible with pytest8
1.7.3 - 25 Feb 2023
* This version has no changes to the installed code.
* This release converts to hatch for packaging and adds a wheel to the
package on pypi.
* CI will now run against python 3.11 as well
1.7.2 - 18 Oct 2022
* This version has no changes to the installed code.
* This release adds the tests to the source distribution put onto pypi.
1.7.1 - 16 Feb 2022
* Adding an option to have self closing tags when the value for that
tag equals certain values
1.7.0 - 16 April, 2020
* Use collections.abc to avoid deprecation warning. Thanks @mangin.
* This library no longer supports Python2 and is only supported for
Python3.6+. Note that the library should still work in Python3.5 as I
have not used f-strings, but the framework I use for the tests is only 3.6+.
1.6.1 - August 27, 2019
* Include readme and LICENSE in the package
1.6 - April 27, 2018
* No code changes
* changed the licence to MIT
* Added more metadata to pypi
* Enabled travis ci
* Updated the tests slightly
1.5
* No changelog was kept before this point.
Development
-----------
To enter a virtualenv with dict2xml and dev requirements installed run::
> source run.sh activate
Tests may be run with::
> ./test.sh
Or::
> ./run.sh tox
Linting and formatting is via::
> ./format
> ./lint