diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..870e654
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ # Configure check for outdated GitHub Actions actions in workflows.
+ # See: https://docs.github.com/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
+ - package-ecosystem: "github-actions"
+ directory: "/" # Check the repository's workflows under /.github/workflows/
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/run-tox.yml b/.github/workflows/run-tox.yml
new file mode 100644
index 0000000..89d8928
--- /dev/null
+++ b/.github/workflows/run-tox.yml
@@ -0,0 +1,30 @@
+name: Run all tox jobs using Python3
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["2.7", "3.3", "3.8", "3.10"]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip setuptools
+ pip install tox-gh-actions
+ - name: Run tox
+ run: |
+ tox
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4a689e2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+**/__pycache__
+*.egg-info
+.coverage*
+.tox
+reports
diff --git a/dicttoxml.py b/dicttoxml.py
index e82acb2..4fd3803 100755
--- a/dicttoxml.py
+++ b/dicttoxml.py
@@ -335,7 +335,7 @@ def convert_list(items, ids, parent, attr_type, item_func, cdata):
elif isinstance(item, iterable):
if not attr_type:
- addline('<%s %s>%s%s>' % (
+ addline('<%s%s>%s%s>' % (
item_name, make_attrstring(attr),
convert_list(item, ids, item_name, attr_type, item_func, cdata),
item_name,
diff --git a/tests/test_types.py b/tests/test_types.py
new file mode 100644
index 0000000..cb925d7
--- /dev/null
+++ b/tests/test_types.py
@@ -0,0 +1,496 @@
+import pytest
+
+from dicttoxml import default_item_func
+from dicttoxml import dicttoxml
+
+
+parametized_root = pytest.mark.parametrize(
+ "root",
+ [
+ pytest.param(True, id="With root"),
+ pytest.param(False, id="Without root"),
+ ],
+)
+
+parametized_custom_root = pytest.mark.parametrize(
+ "custom_root",
+ [
+ pytest.param("root", id="Default root"),
+ pytest.param("a custom root", id="Custom root"),
+ ],
+)
+
+parametized_xml_decl = pytest.mark.parametrize(
+ "xml_declaration",
+ [
+ pytest.param(True, id="With XML declaration"),
+ pytest.param(False, id="Without XML declaration"),
+ ],
+)
+
+parametized_ids = pytest.mark.parametrize(
+ "ids",
+ [
+ pytest.param(True, id="With unique IDs"),
+ pytest.param(False, id="Without unique IDs"),
+ ],
+)
+
+parametized_attr_type = pytest.mark.parametrize(
+ "attr_type",
+ [
+ pytest.param(True, id="With attribute types"),
+ pytest.param(False, id="Without attribute types"),
+ ],
+)
+
+def custom_item_func(parent):
+ """This function is the same as dicttoxml.default_item_func."""
+ return 'item'
+
+parametized_item_func = pytest.mark.parametrize(
+ "item_func",
+ [
+ pytest.param(default_item_func, id="With default item func"),
+ pytest.param(custom_item_func, id="Without custom item func"),
+ ],
+)
+
+parametized_cdata = pytest.mark.parametrize(
+ "cdata",
+ [
+ pytest.param(True, id="With CDATA sections"),
+ pytest.param(False, id="Without CDATA sections"),
+ ],
+)
+
+parametized_include_encoding = pytest.mark.parametrize(
+ "include_encoding",
+ [
+ pytest.param(True, id="With include encoding"),
+ pytest.param(False, id="Without include encoding"),
+ ],
+)
+
+parametized_encoding = pytest.mark.parametrize(
+ "encoding",
+ [
+ pytest.param("UTF-8", id="UTF-8 encoding"),
+ pytest.param("Latin-1", id="Latin1 encoding"),
+ ],
+)
+
+parametized_return_bytes = pytest.mark.parametrize(
+ "return_bytes",
+ [
+ pytest.param(True, id="With return bytes"),
+ pytest.param(False, id="Without return bytes"),
+ ],
+)
+
+
+def _check_dicttoxml(
+ data,
+ type_str,
+ value_str,
+ root,
+ custom_root,
+ xml_declaration,
+ ids,
+ attr_type,
+ item_func,
+ cdata,
+ include_encoding,
+ encoding,
+ return_bytes,
+):
+ # Call the tested function
+ res = dicttoxml(
+ data,
+ root=root,
+ custom_root=custom_root,
+ xml_declaration=xml_declaration,
+ ids=False,
+ attr_type=attr_type,
+ item_func=item_func,
+ cdata=cdata,
+ include_encoding=include_encoding,
+ encoding=encoding,
+ return_bytes=return_bytes,
+ )
+
+ # Build expected result
+ if cdata == True:
+ a_xml = "" % value_str
+ else:
+ a_xml = "%s" % value_str
+
+ div = "a"
+
+ if attr_type == True:
+ a_xml = '<%s type="%s">%s%s>' % (div, type_str, a_xml, div)
+ else:
+ a_xml = "<%s>%s%s>" % (div, a_xml, div)
+
+ expected = ""
+ if root == True:
+ if xml_declaration == True:
+ if include_encoding == False:
+ expected += ''
+ else:
+ expected += '' % (encoding, )
+ expected += "<%s>%s%s>" % (custom_root, a_xml, custom_root)
+ else:
+ expected += a_xml
+
+ if return_bytes:
+ assert res == expected.encode("UTF-8")
+ else:
+ assert res == expected
+
+
+@parametized_root
+@parametized_custom_root
+@parametized_xml_decl
+@parametized_attr_type
+@parametized_item_func
+@parametized_cdata
+@parametized_include_encoding
+@parametized_encoding
+@parametized_return_bytes
+def test_str(
+ root,
+ custom_root,
+ xml_declaration,
+ attr_type,
+ item_func,
+ cdata,
+ include_encoding,
+ encoding,
+ return_bytes,
+):
+ a = {"a": "string"}
+
+ _check_dicttoxml(
+ a,
+ "str",
+ "string",
+ root=root,
+ custom_root=custom_root,
+ xml_declaration=xml_declaration,
+ ids=False, # Not relevant for str
+ attr_type=attr_type,
+ item_func=item_func,
+ cdata=cdata,
+ include_encoding=include_encoding,
+ encoding=encoding,
+ return_bytes=return_bytes,
+ )
+
+
+@parametized_root
+@parametized_custom_root
+@parametized_xml_decl
+@parametized_attr_type
+@parametized_item_func
+@parametized_cdata
+@parametized_include_encoding
+@parametized_encoding
+@parametized_return_bytes
+def test_int(
+ root,
+ custom_root,
+ xml_declaration,
+ attr_type,
+ item_func,
+ cdata,
+ include_encoding,
+ encoding,
+ return_bytes,
+):
+ a = {"a": 1}
+
+ _check_dicttoxml(
+ a,
+ "int",
+ "1",
+ root=root,
+ custom_root=custom_root,
+ xml_declaration=xml_declaration,
+ ids=False, # Not relevant for str
+ attr_type=attr_type,
+ item_func=item_func,
+ cdata=cdata,
+ include_encoding=include_encoding,
+ encoding=encoding,
+ return_bytes=return_bytes,
+ )
+
+
+@parametized_root
+@parametized_custom_root
+@parametized_xml_decl
+@parametized_attr_type
+@parametized_item_func
+@parametized_cdata
+@parametized_include_encoding
+@parametized_encoding
+@parametized_return_bytes
+def test_float(
+ root,
+ custom_root,
+ xml_declaration,
+ attr_type,
+ item_func,
+ cdata,
+ include_encoding,
+ encoding,
+ return_bytes,
+):
+ a = {"a": 1.2}
+
+ _check_dicttoxml(
+ a,
+ "float",
+ "1.2",
+ root=root,
+ custom_root=custom_root,
+ xml_declaration=xml_declaration,
+ ids=False, # Not relevant for str
+ attr_type=attr_type,
+ item_func=item_func,
+ cdata=cdata,
+ include_encoding=include_encoding,
+ encoding=encoding,
+ return_bytes=return_bytes,
+ )
+
+
+@parametized_root
+@parametized_custom_root
+@parametized_xml_decl
+# @parametized_ids
+@parametized_attr_type
+@parametized_item_func
+# @parametized_cdata
+@parametized_include_encoding
+@parametized_encoding
+@parametized_return_bytes
+def test_list(
+ root,
+ custom_root,
+ xml_declaration,
+ # ids,
+ attr_type,
+ item_func,
+ # cdata,
+ include_encoding,
+ encoding,
+ return_bytes,
+):
+ a = {"a": [1, 2]}
+
+ if attr_type == True:
+ expected = '- 1
- 2
'
+ else:
+ expected = "- 1
- 2
"
+
+ _check_dicttoxml(
+ a,
+ "list",
+ expected,
+ root=root,
+ custom_root=custom_root,
+ xml_declaration=xml_declaration,
+ ids=False, # Not tested for now
+ attr_type=attr_type,
+ item_func=item_func,
+ cdata=False, # Not tested for now
+ include_encoding=include_encoding,
+ encoding=encoding,
+ return_bytes=return_bytes,
+ )
+
+
+@parametized_root
+@parametized_custom_root
+@parametized_xml_decl
+# @parametized_ids
+@parametized_attr_type
+@parametized_item_func
+# @parametized_cdata
+@parametized_include_encoding
+@parametized_encoding
+@parametized_return_bytes
+def test_tuple(
+ root,
+ custom_root,
+ xml_declaration,
+ # ids,
+ attr_type,
+ item_func,
+ # cdata,
+ include_encoding,
+ encoding,
+ return_bytes,
+):
+ a = {"a": (1, 2)}
+
+ if attr_type == True:
+ expected = '- 1
- 2
'
+ else:
+ expected = "- 1
- 2
"
+
+ _check_dicttoxml(
+ a,
+ "list",
+ expected,
+ root=root,
+ custom_root=custom_root,
+ xml_declaration=xml_declaration,
+ ids=False, # Not tested for now
+ attr_type=attr_type,
+ item_func=item_func,
+ cdata=False, # Not tested for now
+ include_encoding=include_encoding,
+ encoding=encoding,
+ return_bytes=return_bytes,
+ )
+
+
+@parametized_root
+@parametized_custom_root
+@parametized_xml_decl
+# @parametized_ids
+@parametized_attr_type
+@parametized_item_func
+# @parametized_cdata
+@parametized_include_encoding
+@parametized_encoding
+@parametized_return_bytes
+def test_dict(
+ root,
+ custom_root,
+ xml_declaration,
+ # ids,
+ attr_type,
+ item_func,
+ # cdata,
+ include_encoding,
+ encoding,
+ return_bytes,
+):
+ a = {"a": {"b": 1}}
+
+ if attr_type == True:
+ expected = '1'
+ else:
+ expected = "1"
+
+ _check_dicttoxml(
+ a,
+ "dict",
+ expected,
+ root=root,
+ custom_root=custom_root,
+ xml_declaration=xml_declaration,
+ ids=False, # Not tested for now
+ attr_type=attr_type,
+ item_func=item_func,
+ cdata=False, # Not tested for now
+ include_encoding=include_encoding,
+ encoding=encoding,
+ return_bytes=return_bytes,
+ )
+
+
+@parametized_root
+@parametized_custom_root
+@parametized_xml_decl
+# @parametized_ids
+@parametized_attr_type
+@parametized_item_func
+# @parametized_cdata
+@parametized_include_encoding
+@parametized_encoding
+@parametized_return_bytes
+def test_nested_dict_tuple(
+ root,
+ custom_root,
+ xml_declaration,
+ # ids,
+ attr_type,
+ item_func,
+ # cdata,
+ include_encoding,
+ encoding,
+ return_bytes,
+):
+ a = {"a": {"b": (1, 2)}}
+
+ if attr_type == True:
+ expected = '- 1
- 2
'
+ else:
+ expected = "- 1
- 2
"
+
+ _check_dicttoxml(
+ a,
+ "dict",
+ expected,
+ root=root,
+ custom_root=custom_root,
+ xml_declaration=xml_declaration,
+ ids=False, # Not tested for now
+ attr_type=attr_type,
+ item_func=item_func,
+ cdata=False, # Not tested for now
+ include_encoding=include_encoding,
+ encoding=encoding,
+ return_bytes=return_bytes,
+ )
+
+
+@parametized_root
+@parametized_custom_root
+@parametized_xml_decl
+# @parametized_ids
+@parametized_attr_type
+@parametized_item_func
+# @parametized_cdata
+@parametized_include_encoding
+@parametized_encoding
+@parametized_return_bytes
+def test_nested_tuple_tuple(
+ root,
+ custom_root,
+ xml_declaration,
+ # ids,
+ attr_type,
+ item_func,
+ # cdata,
+ include_encoding,
+ encoding,
+ return_bytes,
+):
+ a = {"a": (1, 2, (3, 4))}
+
+ if attr_type == True:
+ expected = '- 1
- 2
- 3
- 4
'
+ else:
+ expected = "- 1
- 2
- 3
- 4
"
+
+ _check_dicttoxml(
+ a,
+ "list",
+ expected,
+ root=root,
+ custom_root=custom_root,
+ xml_declaration=xml_declaration,
+ ids=False, # Not tested for now
+ attr_type=attr_type,
+ item_func=item_func,
+ cdata=False, # Not tested for now
+ include_encoding=include_encoding,
+ encoding=encoding,
+ return_bytes=return_bytes,
+ )
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..3b490d7
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,47 @@
+[base]
+name = dir_content_diff
+files = {[base]name} tests docs/source/conf.py setup.py
+
+[tox]
+envlist =
+ check-packaging
+ py{27,33,38,310}
+
+minversion = 3.18
+
+[testenv]
+deps =
+ pytest
+ pytest-cov
+ pytest-html
+setenv =
+ COVERAGE_FILE = {env:COVERAGE_FILE:.coverage-{envname}}
+commands = pytest \
+ --basetemp={envtmpdir} \
+ --cov=dicttoxml \
+ --cov-branch \
+ --cov-fail-under=48 \
+ --no-cov-on-fail \
+ --cov-report term-missing \
+ --cov-report html:reports/coverage-{envname} \
+ --cov-report xml:reports/coverage-{envname}.xml \
+ --html reports/pytest-{envname}.html \
+ --junit-xml=reports/pytest-{envname}.xml \
+ --self-contained-html \
+ {posargs}
+
+[testenv:check-packaging]
+skip_install = true
+deps =
+ build
+ twine
+commands =
+ python -m build -o {envtmpdir}/dist
+ twine check {envtmpdir}/dist/*
+
+[gh-actions]
+python =
+ 2.7: py27
+ 3.3: py33
+ 3.8: py38, check-packaging
+ 3.10: py310