From 9e75ca424c0fa610c701e3ed53c0107c8c056aaa Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Fri, 8 Aug 2025 17:13:12 +0530 Subject: [PATCH 1/8] test(dicttoxml): add targeted tests to cover edge branches and XML name handling Adds coverage for CDATA edge, numeric and namespaced keys, list header behavior, @flat handling, Decimal typing, cdata conversion, and id attributes when ids provided. --- tests/test_additional_coverage.py | 101 ++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/test_additional_coverage.py diff --git a/tests/test_additional_coverage.py b/tests/test_additional_coverage.py new file mode 100644 index 0000000..0c96d1f --- /dev/null +++ b/tests/test_additional_coverage.py @@ -0,0 +1,101 @@ +import decimal +from typing import Any + +import pytest + +from json2xml import dicttoxml + + +class TestAdditionalCoverage: + def test_wrap_cdata_handles_cdata_end(self) -> None: + # Ensure CDATA splitting works for "]]>" sequence + text = "a]]>b" + wrapped = dicttoxml.wrap_cdata(text) + assert wrapped == "b]]>" + + def test_make_valid_xml_name_with_int_key(self) -> None: + # Int keys should be converted to n + key, attr = dicttoxml.make_valid_xml_name(123, {}) # type: ignore[arg-type] + assert key == "n123" + assert attr == {} + + def test_make_valid_xml_name_namespace_flat(self) -> None: + # Namespaced key with @flat suffix should be considered valid as-is + key_in = "ns:key@flat" + key_out, attr = dicttoxml.make_valid_xml_name(key_in, {}) + assert key_out == key_in + assert attr == {} + + def test_dict2xml_str_parent_list_with_attrs_and_no_wrap(self) -> None: + # When inside list context with list_headers=True and item_wrap=False, + # attributes belong to the parent element header + item = {"@attrs": {"a": "b"}, "@val": "X"} + xml = dicttoxml.dict2xml_str( + attr_type=False, + attr={}, + item=item, + item_func=lambda _p: "item", + cdata=False, + item_name="ignored", + item_wrap=False, + parentIsList=True, + parent="Parent", + list_headers=True, + ) + assert xml == 'X' + + def test_dict2xml_str_with_flat_flag_in_item(self) -> None: + # If @flat=True, the subtree should not be wrapped + item = {"@val": "text", "@flat": True} + xml = dicttoxml.dict2xml_str( + attr_type=False, + attr={}, + item=item, + item_func=lambda _p: "item", + cdata=False, + item_name="ignored", + item_wrap=True, + parentIsList=False, + ) + assert xml == "text" + + def test_list2xml_str_returns_subtree_when_list_headers_true(self) -> None: + # list_headers=True should return subtree directly from convert_list + xml = dicttoxml.list2xml_str( + attr_type=False, + attr={}, + item=["a"], + item_func=lambda _p: "item", + cdata=False, + item_name="test", + item_wrap=True, + list_headers=True, + ) + assert xml == "a" + + def test_get_xml_type_with_decimal_number(self) -> None: + # Decimal is a numbers.Number but not int/float + value = decimal.Decimal("5") + assert dicttoxml.get_xml_type(value) == "number" + # And convert_kv should mark it as type="number" + out = dicttoxml.convert_kv("key", value, attr_type=True) + assert out == '5' + + def test_dicttoxml_cdata_with_cdata_end_sequence(self) -> None: + data = {"key": "a]]>b"} + out = dicttoxml.dicttoxml(data, root=False, attr_type=False, cdata=True).decode() + assert out == "b]]>" + + def test_convert_dict_with_ids_adds_id_attributes(self) -> None: + obj: dict[str, Any] = {"a": 1, "b": 2} + xml = dicttoxml.convert_dict( + obj=obj, + ids=["seed"], + parent="root", + attr_type=False, + item_func=lambda _p: "item", + cdata=False, + item_wrap=True, + ) + # Both elements should carry some id attribute + assert xml.count(' id="') == 2 From 8f19e8a6ac8ee2f4a7dd0064475ff7ddf9e5d7db Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Fri, 8 Aug 2025 17:14:31 +0530 Subject: [PATCH 2/8] refactor(dicttoxml): improve typing and xml type detection; simplify get_unique_id API - Accept optional ids list to avoid collisions deterministically in tests - Replace legacy type name checks with direct 'str'/'int' - Update tests to use monkeypatch for duplicate id simulation --- json2xml/dicttoxml.py | 10 +++++---- tests/test_dict2xml.py | 49 +++++++----------------------------------- 2 files changed, 14 insertions(+), 45 deletions(-) diff --git a/json2xml/dicttoxml.py b/json2xml/dicttoxml.py index af32da4..0ecba18 100644 --- a/json2xml/dicttoxml.py +++ b/json2xml/dicttoxml.py @@ -31,17 +31,19 @@ def make_id(element: str, start: int = 100000, end: int = 999999) -> str: return f"{element}_{safe_random.randint(start, end)}" -def get_unique_id(element: str) -> str: +def get_unique_id(element: str, ids: list[str] | None = None) -> str: """ Generate a unique ID for a given element. Args: element (str): The element to generate an ID for. + ids (list[str] | None, optional): A list of existing IDs to avoid duplicates. Defaults to None. Returns: str: The unique ID. """ - ids: list[str] = [] # initialize list of unique ids + if ids is None: + ids = [] this_id = make_id(element) dup = True while dup: @@ -78,9 +80,9 @@ def get_xml_type(val: ELEMENT) -> str: str: The XML type. """ if val is not None: - if type(val).__name__ in ("str", "unicode"): + if type(val).__name__ == "str": return "str" - if type(val).__name__ in ("int", "long"): + if type(val).__name__ == "int": return "int" if type(val).__name__ == "float": return "float" diff --git a/tests/test_dict2xml.py b/tests/test_dict2xml.py index df770a9..d63efeb 100644 --- a/tests/test_dict2xml.py +++ b/tests/test_dict2xml.py @@ -1,6 +1,7 @@ import datetime import numbers from typing import TYPE_CHECKING, Any +from unittest.mock import Mock import pytest @@ -774,50 +775,16 @@ def test_dicttoxml_with_cdata(self) -> None: result = dicttoxml.dicttoxml(data, cdata=True, attr_type=False, root=False) assert b"" == result - def test_get_unique_id_with_duplicates(self) -> None: + def test_get_unique_id_with_duplicates(self, monkeypatch: "MonkeyPatch") -> None: """Test get_unique_id when duplicates are generated.""" - # We need to modify the original get_unique_id to simulate a pre-existing ID list - import json2xml.dicttoxml as module + ids = ["existing_id"] + make_id_mock = Mock(side_effect=["existing_id", "new_id"]) + monkeypatch.setattr(dicttoxml, "make_id", make_id_mock) - # Save original function - original_get_unique_id = module.get_unique_id - - # Track make_id calls - call_count = 0 - original_make_id = module.make_id - - def mock_make_id(element: str, start: int = 100000, end: int = 999999) -> str: - nonlocal call_count - call_count += 1 - if call_count == 1: - return "test_123456" # First call - will collide - else: - return "test_789012" # Second call - unique - - # Patch get_unique_id to use a pre-populated ids list - def patched_get_unique_id(element: str) -> str: - # Start with a pre-existing ID to force collision - ids = ["test_123456"] - this_id = module.make_id(element) - dup = True - while dup: - if this_id not in ids: - dup = False - ids.append(this_id) - else: - this_id = module.make_id(element) # This exercises line 52 - return ids[-1] - - module.make_id = mock_make_id - module.get_unique_id = patched_get_unique_id + unique_id = dicttoxml.get_unique_id("some_element", ids=ids) - try: - result = dicttoxml.get_unique_id("test") - assert result == "test_789012" - assert call_count == 2 - finally: - module.make_id = original_make_id - module.get_unique_id = original_get_unique_id + assert unique_id == "new_id" + assert make_id_mock.call_count == 2 def test_convert_with_bool_direct(self) -> None: """Test convert function with boolean input directly.""" From 177a6d5965cfc3ccfef2d35bc2518539e9d45ff5 Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Tue, 12 Aug 2025 15:11:06 +0530 Subject: [PATCH 3/8] fix: remove py --- pyproject.toml | 1 - uv.lock | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 533a7b8..3f2a200 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,6 @@ dependencies = [ "pytest", "pytest-cov", "coverage", - "py", "setuptools", ] diff --git a/uv.lock b/uv.lock index 15f0b48..08f541a 100644 --- a/uv.lock +++ b/uv.lock @@ -110,12 +110,11 @@ wheels = [ [[package]] name = "json2xml" -version = "5.1.0" +version = "5.2.0" source = { editable = "." } dependencies = [ { name = "coverage" }, { name = "defusedxml" }, - { name = "py" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "setuptools" }, @@ -133,7 +132,6 @@ test = [ requires-dist = [ { name = "coverage" }, { name = "defusedxml" }, - { name = "py" }, { name = "py", marker = "extra == 'test'", specifier = "==1.11.0" }, { name = "pytest" }, { name = "pytest", marker = "extra == 'test'", specifier = "==7.0.1" }, From d0811865c1581e71ad1e22a467e1d44d102c408f Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Tue, 12 Aug 2025 15:12:52 +0530 Subject: [PATCH 4/8] chore: remove py everywhere --- json2xml/__init__.py | 2 +- pyproject.toml | 3 +-- uv.lock | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/json2xml/__init__.py b/json2xml/__init__.py index fc587a5..53e5f5b 100644 --- a/json2xml/__init__.py +++ b/json2xml/__init__.py @@ -2,5 +2,5 @@ __author__ = """Vinit Kumar""" __email__ = "mail@vinitkumar.me" -__version__ = "5.2.0" +__version__ = "5.2.1" diff --git a/pyproject.toml b/pyproject.toml index 3f2a200..19ab880 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "json2xml" -version = "5.2.0" # Replace with the dynamic version if needed +version = "5.2.1" # Replace with the dynamic version if needed description = "Simple Python Library to convert JSON to XML" readme = "README.rst" requires-python = ">=3.10" @@ -46,7 +46,6 @@ include = ["json2xml"] [project.optional-dependencies] test = [ "pytest==7.0.1", - "py==1.11.0" ] [tool.pytest.ini_options] diff --git a/uv.lock b/uv.lock index 08f541a..29559dd 100644 --- a/uv.lock +++ b/uv.lock @@ -110,7 +110,7 @@ wheels = [ [[package]] name = "json2xml" -version = "5.2.0" +version = "5.2.1" source = { editable = "." } dependencies = [ { name = "coverage" }, @@ -124,7 +124,6 @@ dependencies = [ [package.optional-dependencies] test = [ - { name = "py" }, { name = "pytest" }, ] @@ -132,7 +131,6 @@ test = [ requires-dist = [ { name = "coverage" }, { name = "defusedxml" }, - { name = "py", marker = "extra == 'test'", specifier = "==1.11.0" }, { name = "pytest" }, { name = "pytest", marker = "extra == 'test'", specifier = "==7.0.1" }, { name = "pytest-cov" }, From 9838d1bc8d1e5d3a6650ab2ca0ab722c7eaf9204 Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Tue, 12 Aug 2025 15:20:31 +0530 Subject: [PATCH 5/8] feat: update docs requirements --- docs/requirements.in | 19 ++++++++------- docs/requirements.txt | 57 ++++++++++++++++++++++--------------------- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/docs/requirements.in b/docs/requirements.in index 30ecbf0..312d299 100644 --- a/docs/requirements.in +++ b/docs/requirements.in @@ -1,14 +1,15 @@ -furo==2024.8.6 -sphinx -sphinx-autobuild +furo==2025.7.19 +sphinx==8.2.3 +sphinx-autobuild==2024.10.3 # if using typehints -sphinx-autodoc-typehints +sphinx-autodoc-typehints==3.2.0 -mock -autodoc +mock==5.2.0 +autodoc==0.5.0 -defusedxml -tornado +defusedxml==0.7.1 +tornado==6.5.2 jinja2>=3.1.6 -idna +idna==3.10 +starlette>=0.47.2 diff --git a/docs/requirements.txt b/docs/requirements.txt index a65a915..5fd9bd2 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,9 +1,11 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # -# pip-compile requirements.in +# pip-compile # +accessible-pygments==0.0.5 + # via furo alabaster==1.0.0 # via sphinx anyio==4.8.0 @@ -11,7 +13,7 @@ anyio==4.8.0 # starlette # watchfiles autodoc==0.5.0 - # via -r docs/requirements.in + # via -r requirements.in babel==2.17.0 # via sphinx beautifulsoup4==4.13.3 @@ -29,55 +31,58 @@ colorama==0.4.6 decorator==5.1.1 # via autodoc defusedxml==0.7.1 - # via -r docs/requirements.in + # via -r requirements.in docutils==0.21.2 # via sphinx -exceptiongroup==1.3.0 - # via anyio -furo==2024.8.6 - # via -r docs/requirements.in +furo==2025.7.19 + # via -r requirements.in h11==0.16.0 # via uvicorn idna==3.10 # via - # -r docs/requirements.in + # -r requirements.in # anyio # requests imagesize==1.4.1 # via sphinx jinja2==3.1.6 # via - # -r docs/requirements.in + # -r requirements.in # sphinx +legacy-cgi==2.6.3 + # via webob markupsafe==3.0.2 # via jinja2 -mock==5.1.0 - # via -r docs/requirements.in +mock==5.2.0 + # via -r requirements.in packaging==24.2 # via sphinx pygments==2.19.1 # via + # accessible-pygments # furo # sphinx requests==2.32.4 # via sphinx +roman-numerals-py==3.1.0 + # via sphinx sniffio==1.3.1 # via anyio snowballstemmer==2.2.0 # via sphinx soupsieve==2.6 # via beautifulsoup4 -sphinx==8.1.3 +sphinx==8.2.3 # via - # -r docs/requirements.in + # -r requirements.in # furo # sphinx-autobuild # sphinx-autodoc-typehints # sphinx-basic-ng sphinx-autobuild==2024.10.3 - # via -r docs/requirements.in -sphinx-autodoc-typehints==3.0.1 - # via -r docs/requirements.in + # via -r requirements.in +sphinx-autodoc-typehints==3.2.0 + # via -r requirements.in sphinx-basic-ng==1.0.0b2 # via furo sphinxcontrib-applehelp==2.0.0 @@ -92,18 +97,14 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx -starlette==0.45.3 - # via sphinx-autobuild -tomli==2.2.1 - # via sphinx -tornado==6.5.1 - # via -r docs/requirements.in -typing-extensions==4.12.2 +starlette==0.47.2 # via - # anyio - # beautifulsoup4 - # exceptiongroup - # uvicorn + # -r requirements.in + # sphinx-autobuild +tornado==6.5.2 + # via -r requirements.in +typing-extensions==4.12.2 + # via beautifulsoup4 urllib3==2.5.0 # via requests uvicorn==0.34.0 From d797884029eee5cff49d4815bcd4152acacc88e7 Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Tue, 12 Aug 2025 15:30:03 +0530 Subject: [PATCH 6/8] feat: update packages --- requirements-dev.in | 18 +++++++++--------- requirements-dev.txt | 33 +++++++++++++++------------------ requirements.in | 4 ++-- requirements.txt | 2 +- 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/requirements-dev.in b/requirements-dev.in index bcb2341..19e4e7a 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -1,11 +1,11 @@ # When installing dev dependencies also install the user dependencies -r requirements.in -xmltodict>=0.12.0 -pytest -pytest-cov -pytest-xdist>=3.5.0 -coverage -ruff>=0.3.0 -setuptools -mypy>=1.0.0 -types-setuptools +xmltodict>=0.14.2 +pytest==8.4.1 +pytest-cov==6.2.1 +pytest-xdist==3.8.0 +coverage==7.10.3 +ruff==0.12.8 +setuptools==80.9.0 +mypy==1.17.1 +types-setuptools==80.9.0.20250809 diff --git a/requirements-dev.txt b/requirements-dev.txt index 66e6286..088aeb8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,51 +1,48 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # # pip-compile requirements-dev.in # -coverage[toml]==7.6.11 +coverage[toml]==7.10.3 # via # -r requirements-dev.in # pytest-cov defusedxml==0.7.1 # via -r requirements.in -exceptiongroup==1.3.0 - # via pytest execnet==2.1.1 # via pytest-xdist iniconfig==2.0.0 # via pytest -mypy==1.15.0 +mypy==1.17.1 # via -r requirements-dev.in mypy-extensions==1.0.0 # via mypy packaging==24.2 # via pytest +pathspec==0.12.1 + # via mypy pluggy==1.5.0 + # via + # pytest + # pytest-cov +pygments==2.19.2 # via pytest -pytest==8.3.4 +pytest==8.4.1 # via # -r requirements-dev.in # pytest-cov # pytest-xdist -pytest-cov==6.0.0 +pytest-cov==6.2.1 # via -r requirements-dev.in -pytest-xdist==3.7.0 +pytest-xdist==3.8.0 # via -r requirements-dev.in -ruff==0.11.13 +ruff==0.12.8 # via -r requirements-dev.in -tomli==2.2.1 - # via - # coverage - # mypy - # pytest -types-setuptools==80.9.0.20250529 +types-setuptools==80.9.0.20250809 # via -r requirements-dev.in typing-extensions==4.12.2 - # via - # exceptiongroup - # mypy + # via mypy urllib3==2.5.0 # via -r requirements.in xmltodict==0.14.2 diff --git a/requirements.in b/requirements.in index be550cc..9ed5639 100644 --- a/requirements.in +++ b/requirements.in @@ -1,3 +1,3 @@ -defusedxml -urllib3 +defusedxml==0.7.1 +urllib3==2.5.0 diff --git a/requirements.txt b/requirements.txt index b9d39c0..1d61858 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # # pip-compile From 483390aaf447b82a8fee3e1b1dae1220a4bb51ef Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Tue, 12 Aug 2025 15:35:06 +0530 Subject: [PATCH 7/8] chore: remove files --- json2xml/dicttoxml.py | 10 ++++----- tests/test_dict2xml.py | 49 +++++++++++++++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/json2xml/dicttoxml.py b/json2xml/dicttoxml.py index 0ecba18..af32da4 100644 --- a/json2xml/dicttoxml.py +++ b/json2xml/dicttoxml.py @@ -31,19 +31,17 @@ def make_id(element: str, start: int = 100000, end: int = 999999) -> str: return f"{element}_{safe_random.randint(start, end)}" -def get_unique_id(element: str, ids: list[str] | None = None) -> str: +def get_unique_id(element: str) -> str: """ Generate a unique ID for a given element. Args: element (str): The element to generate an ID for. - ids (list[str] | None, optional): A list of existing IDs to avoid duplicates. Defaults to None. Returns: str: The unique ID. """ - if ids is None: - ids = [] + ids: list[str] = [] # initialize list of unique ids this_id = make_id(element) dup = True while dup: @@ -80,9 +78,9 @@ def get_xml_type(val: ELEMENT) -> str: str: The XML type. """ if val is not None: - if type(val).__name__ == "str": + if type(val).__name__ in ("str", "unicode"): return "str" - if type(val).__name__ == "int": + if type(val).__name__ in ("int", "long"): return "int" if type(val).__name__ == "float": return "float" diff --git a/tests/test_dict2xml.py b/tests/test_dict2xml.py index d63efeb..df770a9 100644 --- a/tests/test_dict2xml.py +++ b/tests/test_dict2xml.py @@ -1,7 +1,6 @@ import datetime import numbers from typing import TYPE_CHECKING, Any -from unittest.mock import Mock import pytest @@ -775,16 +774,50 @@ def test_dicttoxml_with_cdata(self) -> None: result = dicttoxml.dicttoxml(data, cdata=True, attr_type=False, root=False) assert b"" == result - def test_get_unique_id_with_duplicates(self, monkeypatch: "MonkeyPatch") -> None: + def test_get_unique_id_with_duplicates(self) -> None: """Test get_unique_id when duplicates are generated.""" - ids = ["existing_id"] - make_id_mock = Mock(side_effect=["existing_id", "new_id"]) - monkeypatch.setattr(dicttoxml, "make_id", make_id_mock) + # We need to modify the original get_unique_id to simulate a pre-existing ID list + import json2xml.dicttoxml as module - unique_id = dicttoxml.get_unique_id("some_element", ids=ids) + # Save original function + original_get_unique_id = module.get_unique_id + + # Track make_id calls + call_count = 0 + original_make_id = module.make_id + + def mock_make_id(element: str, start: int = 100000, end: int = 999999) -> str: + nonlocal call_count + call_count += 1 + if call_count == 1: + return "test_123456" # First call - will collide + else: + return "test_789012" # Second call - unique + + # Patch get_unique_id to use a pre-populated ids list + def patched_get_unique_id(element: str) -> str: + # Start with a pre-existing ID to force collision + ids = ["test_123456"] + this_id = module.make_id(element) + dup = True + while dup: + if this_id not in ids: + dup = False + ids.append(this_id) + else: + this_id = module.make_id(element) # This exercises line 52 + return ids[-1] + + module.make_id = mock_make_id + module.get_unique_id = patched_get_unique_id - assert unique_id == "new_id" - assert make_id_mock.call_count == 2 + try: + result = dicttoxml.get_unique_id("test") + assert result == "test_789012" + assert call_count == 2 + finally: + module.make_id = original_make_id + module.get_unique_id = original_get_unique_id def test_convert_with_bool_direct(self) -> None: """Test convert function with boolean input directly.""" From 4e19a91d33355b656f82ad8682a1614424768456 Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Tue, 12 Aug 2025 15:35:46 +0530 Subject: [PATCH 8/8] fix: remove garbage --- tests/test_additional_coverage.py | 101 ------------------------------ 1 file changed, 101 deletions(-) delete mode 100644 tests/test_additional_coverage.py diff --git a/tests/test_additional_coverage.py b/tests/test_additional_coverage.py deleted file mode 100644 index 0c96d1f..0000000 --- a/tests/test_additional_coverage.py +++ /dev/null @@ -1,101 +0,0 @@ -import decimal -from typing import Any - -import pytest - -from json2xml import dicttoxml - - -class TestAdditionalCoverage: - def test_wrap_cdata_handles_cdata_end(self) -> None: - # Ensure CDATA splitting works for "]]>" sequence - text = "a]]>b" - wrapped = dicttoxml.wrap_cdata(text) - assert wrapped == "b]]>" - - def test_make_valid_xml_name_with_int_key(self) -> None: - # Int keys should be converted to n - key, attr = dicttoxml.make_valid_xml_name(123, {}) # type: ignore[arg-type] - assert key == "n123" - assert attr == {} - - def test_make_valid_xml_name_namespace_flat(self) -> None: - # Namespaced key with @flat suffix should be considered valid as-is - key_in = "ns:key@flat" - key_out, attr = dicttoxml.make_valid_xml_name(key_in, {}) - assert key_out == key_in - assert attr == {} - - def test_dict2xml_str_parent_list_with_attrs_and_no_wrap(self) -> None: - # When inside list context with list_headers=True and item_wrap=False, - # attributes belong to the parent element header - item = {"@attrs": {"a": "b"}, "@val": "X"} - xml = dicttoxml.dict2xml_str( - attr_type=False, - attr={}, - item=item, - item_func=lambda _p: "item", - cdata=False, - item_name="ignored", - item_wrap=False, - parentIsList=True, - parent="Parent", - list_headers=True, - ) - assert xml == 'X' - - def test_dict2xml_str_with_flat_flag_in_item(self) -> None: - # If @flat=True, the subtree should not be wrapped - item = {"@val": "text", "@flat": True} - xml = dicttoxml.dict2xml_str( - attr_type=False, - attr={}, - item=item, - item_func=lambda _p: "item", - cdata=False, - item_name="ignored", - item_wrap=True, - parentIsList=False, - ) - assert xml == "text" - - def test_list2xml_str_returns_subtree_when_list_headers_true(self) -> None: - # list_headers=True should return subtree directly from convert_list - xml = dicttoxml.list2xml_str( - attr_type=False, - attr={}, - item=["a"], - item_func=lambda _p: "item", - cdata=False, - item_name="test", - item_wrap=True, - list_headers=True, - ) - assert xml == "a" - - def test_get_xml_type_with_decimal_number(self) -> None: - # Decimal is a numbers.Number but not int/float - value = decimal.Decimal("5") - assert dicttoxml.get_xml_type(value) == "number" - # And convert_kv should mark it as type="number" - out = dicttoxml.convert_kv("key", value, attr_type=True) - assert out == '5' - - def test_dicttoxml_cdata_with_cdata_end_sequence(self) -> None: - data = {"key": "a]]>b"} - out = dicttoxml.dicttoxml(data, root=False, attr_type=False, cdata=True).decode() - assert out == "b]]>" - - def test_convert_dict_with_ids_adds_id_attributes(self) -> None: - obj: dict[str, Any] = {"a": 1, "b": 2} - xml = dicttoxml.convert_dict( - obj=obj, - ids=["seed"], - parent="root", - attr_type=False, - item_func=lambda _p: "item", - cdata=False, - item_wrap=True, - ) - # Both elements should carry some id attribute - assert xml.count(' id="') == 2