From 9e75ca424c0fa610c701e3ed53c0107c8c056aaa Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Fri, 8 Aug 2025 17:13:12 +0530 Subject: [PATCH 1/3] 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/3] 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 387c1b165648897df915ab5b9422bf582dc5b96e Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Fri, 8 Aug 2025 17:18:07 +0530 Subject: [PATCH 3/3] test: relax typing in Decimal tests using cast to satisfy mypy --- tests/test_additional_coverage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_additional_coverage.py b/tests/test_additional_coverage.py index 0c96d1f..c8cb345 100644 --- a/tests/test_additional_coverage.py +++ b/tests/test_additional_coverage.py @@ -1,5 +1,5 @@ import decimal -from typing import Any +from typing import Any, cast import pytest @@ -76,9 +76,9 @@ def test_list2xml_str_returns_subtree_when_list_headers_true(self) -> None: 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" + assert dicttoxml.get_xml_type(cast(Any, value)) == "number" # And convert_kv should mark it as type="number" - out = dicttoxml.convert_kv("key", value, attr_type=True) + out = dicttoxml.convert_kv("key", cast(Any, value), attr_type=True) assert out == '5' def test_dicttoxml_cdata_with_cdata_end_sequence(self) -> None: