json2xml 6.0.4__tar.gz → 6.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {json2xml-6.0.4 → json2xml-6.0.6}/HISTORY.rst +23 -1
  2. {json2xml-6.0.4 → json2xml-6.0.6}/PKG-INFO +16 -16
  3. {json2xml-6.0.4 → json2xml-6.0.6}/README.rst +15 -15
  4. {json2xml-6.0.4 → json2xml-6.0.6}/json2xml/__init__.py +1 -2
  5. {json2xml-6.0.4 → json2xml-6.0.6}/json2xml/cli.py +2 -0
  6. {json2xml-6.0.4 → json2xml-6.0.6}/json2xml/dicttoxml.py +9 -6
  7. {json2xml-6.0.4 → json2xml-6.0.6}/json2xml/dicttoxml_fast.py +9 -6
  8. {json2xml-6.0.4 → json2xml-6.0.6}/json2xml/json2xml.py +3 -0
  9. {json2xml-6.0.4 → json2xml-6.0.6}/json2xml/utils.py +2 -1
  10. {json2xml-6.0.4 → json2xml-6.0.6}/json2xml.egg-info/PKG-INFO +16 -16
  11. {json2xml-6.0.4 → json2xml-6.0.6}/pyproject.toml +1 -1
  12. {json2xml-6.0.4 → json2xml-6.0.6}/tests/conftest.py +1 -1
  13. {json2xml-6.0.4 → json2xml-6.0.6}/tests/test_cli.py +2 -0
  14. {json2xml-6.0.4 → json2xml-6.0.6}/tests/test_dict2xml.py +112 -43
  15. {json2xml-6.0.4 → json2xml-6.0.6}/tests/test_json2xml.py +1 -6
  16. {json2xml-6.0.4 → json2xml-6.0.6}/tests/test_rust_dicttoxml.py +6 -6
  17. {json2xml-6.0.4 → json2xml-6.0.6}/AUTHORS.rst +0 -0
  18. {json2xml-6.0.4 → json2xml-6.0.6}/CONTRIBUTING.rst +0 -0
  19. {json2xml-6.0.4 → json2xml-6.0.6}/LICENSE +0 -0
  20. {json2xml-6.0.4 → json2xml-6.0.6}/MANIFEST.in +0 -0
  21. {json2xml-6.0.4 → json2xml-6.0.6}/docs/Makefile +0 -0
  22. {json2xml-6.0.4 → json2xml-6.0.6}/docs/authors.rst +0 -0
  23. {json2xml-6.0.4 → json2xml-6.0.6}/docs/benchmarks.rst +0 -0
  24. {json2xml-6.0.4 → json2xml-6.0.6}/docs/conf.py +0 -0
  25. {json2xml-6.0.4 → json2xml-6.0.6}/docs/contributing.rst +0 -0
  26. {json2xml-6.0.4 → json2xml-6.0.6}/docs/history.rst +0 -0
  27. {json2xml-6.0.4 → json2xml-6.0.6}/docs/index.rst +0 -0
  28. {json2xml-6.0.4 → json2xml-6.0.6}/docs/installation.rst +0 -0
  29. {json2xml-6.0.4 → json2xml-6.0.6}/docs/json2xml.rst +0 -0
  30. {json2xml-6.0.4 → json2xml-6.0.6}/docs/make.bat +0 -0
  31. {json2xml-6.0.4 → json2xml-6.0.6}/docs/modules.rst +0 -0
  32. {json2xml-6.0.4 → json2xml-6.0.6}/docs/readme.rst +0 -0
  33. {json2xml-6.0.4 → json2xml-6.0.6}/docs/usage.rst +0 -0
  34. {json2xml-6.0.4 → json2xml-6.0.6}/json2xml.egg-info/SOURCES.txt +0 -0
  35. {json2xml-6.0.4 → json2xml-6.0.6}/json2xml.egg-info/dependency_links.txt +0 -0
  36. {json2xml-6.0.4 → json2xml-6.0.6}/json2xml.egg-info/entry_points.txt +0 -0
  37. {json2xml-6.0.4 → json2xml-6.0.6}/json2xml.egg-info/requires.txt +0 -0
  38. {json2xml-6.0.4 → json2xml-6.0.6}/json2xml.egg-info/top_level.txt +0 -0
  39. {json2xml-6.0.4 → json2xml-6.0.6}/requirements.in +0 -0
  40. {json2xml-6.0.4 → json2xml-6.0.6}/setup.cfg +0 -0
  41. {json2xml-6.0.4 → json2xml-6.0.6}/setup.py +0 -0
  42. {json2xml-6.0.4 → json2xml-6.0.6}/tests/__init__.py +0 -0
  43. {json2xml-6.0.4 → json2xml-6.0.6}/tests/test_missing_coverage.py +0 -0
  44. {json2xml-6.0.4 → json2xml-6.0.6}/tests/test_utils.py +0 -0
@@ -1,3 +1,26 @@
1
+
2
+ 6.0.6 / 2026-04-08
3
+ ==================
4
+
5
+ * fix: review cleanup (#281)
6
+ * fix: remove needless line (#280)
7
+ * docs: add lat.md support to the codebase (#279)
8
+ * chore(deps): bump pygments from 2.19.1 to 2.20.0 in /docs (#278)
9
+ * fix: upgrade Python workflow to 3.12
10
+
11
+
12
+ 6.0.5 / 2026-03-12
13
+ ==================
14
+
15
+ * fix: prevent double-escaping of XML attribute values for invalid key names
16
+ * fix: use Python-compatible float formatting in the Rust backend
17
+ * fix: propagate iteration errors instead of silently swallowing them
18
+ * fix: validate ``custom_root`` as a valid XML element name
19
+ * refactor: reduce Rust writer allocations and remove duplicated dispatch logic
20
+ * chore: bump pyo3 and rust binding versions
21
+ * fix: add missing ``xmltodict`` dependency to Rust CI workflow
22
+
23
+
1
24
  History
2
25
  =======
3
26
 
@@ -582,4 +605,3 @@ v3.0.0 / 2019-02-26
582
605
  * Cleanup Readme.md
583
606
  * Update issue templates
584
607
  * fix vulnerabilities in requests
585
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: json2xml
3
- Version: 6.0.4
3
+ Version: 6.0.6
4
4
  Summary: Simple Python Library to convert JSON to XML
5
5
  Author-email: Vinit Kumar <mail@vinitkumar.me>
6
6
  License: Apache Software License 2.0
@@ -79,7 +79,7 @@ Installation
79
79
 
80
80
  pip install json2xml
81
81
 
82
- **With Native Rust Acceleration (28x faster)**
82
+ **With Native Rust Acceleration (up to 149x faster)**
83
83
 
84
84
  For maximum performance, install the optional Rust extension:
85
85
 
@@ -91,7 +91,7 @@ For maximum performance, install the optional Rust extension:
91
91
  # Or install the Rust extension separately
92
92
  pip install json2xml-rs
93
93
 
94
- The Rust extension provides **28x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
94
+ The Rust extension provides **75-149x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
95
95
 
96
96
  **As a CLI Tool**
97
97
 
@@ -337,7 +337,7 @@ Using tools directly:
337
337
 
338
338
  **Rust Extension Development**
339
339
 
340
- The optional Rust extension (``json2xml-rs``) provides 29x faster performance. To develop or build the Rust extension:
340
+ The optional Rust extension (``json2xml-rs``) provides up to 149x faster performance. To develop or build the Rust extension:
341
341
 
342
342
  Prerequisites:
343
343
 
@@ -464,21 +464,21 @@ For users who need maximum performance within Python, json2xml includes an optio
464
464
  - Rust Extension
465
465
  - Speedup
466
466
  * - **Small JSON** (47 bytes)
467
- - 40µs
468
- - 1.5µs
469
- - **27x**
467
+ - 78µs
468
+ - 1.05µs
469
+ - **75x**
470
470
  * - **Medium JSON** (3.2 KB)
471
- - 2.1ms
472
- - 71µs
473
- - **30x**
471
+ - 2.15ms
472
+ - 15µs
473
+ - **139x**
474
474
  * - **Large JSON** (32 KB)
475
- - 21ms
476
- - 740µs
477
- - **28x**
475
+ - 22ms
476
+ - 151µs
477
+ - **146x**
478
478
  * - **Very Large JSON** (323 KB)
479
- - 213ms
480
- - 7.5ms
481
- - **28x**
479
+ - 219ms
480
+ - 1.47ms
481
+ - **149x**
482
482
 
483
483
  **Usage with Rust Extension:**
484
484
 
@@ -43,7 +43,7 @@ Installation
43
43
 
44
44
  pip install json2xml
45
45
 
46
- **With Native Rust Acceleration (28x faster)**
46
+ **With Native Rust Acceleration (up to 149x faster)**
47
47
 
48
48
  For maximum performance, install the optional Rust extension:
49
49
 
@@ -55,7 +55,7 @@ For maximum performance, install the optional Rust extension:
55
55
  # Or install the Rust extension separately
56
56
  pip install json2xml-rs
57
57
 
58
- The Rust extension provides **28x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
58
+ The Rust extension provides **75-149x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
59
59
 
60
60
  **As a CLI Tool**
61
61
 
@@ -301,7 +301,7 @@ Using tools directly:
301
301
 
302
302
  **Rust Extension Development**
303
303
 
304
- The optional Rust extension (``json2xml-rs``) provides 29x faster performance. To develop or build the Rust extension:
304
+ The optional Rust extension (``json2xml-rs``) provides up to 149x faster performance. To develop or build the Rust extension:
305
305
 
306
306
  Prerequisites:
307
307
 
@@ -428,21 +428,21 @@ For users who need maximum performance within Python, json2xml includes an optio
428
428
  - Rust Extension
429
429
  - Speedup
430
430
  * - **Small JSON** (47 bytes)
431
- - 40µs
432
- - 1.5µs
433
- - **27x**
431
+ - 78µs
432
+ - 1.05µs
433
+ - **75x**
434
434
  * - **Medium JSON** (3.2 KB)
435
- - 2.1ms
436
- - 71µs
437
- - **30x**
435
+ - 2.15ms
436
+ - 15µs
437
+ - **139x**
438
438
  * - **Large JSON** (32 KB)
439
- - 21ms
440
- - 740µs
441
- - **28x**
439
+ - 22ms
440
+ - 151µs
441
+ - **146x**
442
442
  * - **Very Large JSON** (323 KB)
443
- - 213ms
444
- - 7.5ms
445
- - **28x**
443
+ - 219ms
444
+ - 1.47ms
445
+ - **149x**
446
446
 
447
447
  **Usage with Rust Extension:**
448
448
 
@@ -2,5 +2,4 @@
2
2
 
3
3
  __author__ = """Vinit Kumar"""
4
4
  __email__ = "mail@vinitkumar.me"
5
- __version__ = "6.0.4"
6
-
5
+ __version__ = "6.0.6"
@@ -60,6 +60,7 @@ AUTHOR = "Vinit Kumar"
60
60
  EMAIL = "mail@vinitkumar.me"
61
61
 
62
62
 
63
+ # @lat: [[architecture#CLI entrypoint]]
63
64
  def create_parser() -> argparse.ArgumentParser:
64
65
  """Create and configure the argument parser."""
65
66
  parser = argparse.ArgumentParser(
@@ -228,6 +229,7 @@ Examples:
228
229
  return parser
229
230
 
230
231
 
232
+ # @lat: [[behavior#Input readers]]
231
233
  def read_input(args: argparse.Namespace) -> dict[str, Any] | list[Any]:
232
234
  """
233
235
  Read JSON input from the specified source.
@@ -12,6 +12,7 @@ from typing import Any, Union, cast
12
12
  from defusedxml.minidom import parseString
13
13
 
14
14
  # Create a safe random number generator
15
+ _SAFE_RANDOM = SystemRandom()
15
16
 
16
17
  # Set up logging
17
18
  LOG = logging.getLogger("dicttoxml")
@@ -29,8 +30,7 @@ def make_id(element: str, start: int = 100000, end: int = 999999) -> str:
29
30
  Returns:
30
31
  str: The generated ID.
31
32
  """
32
- safe_random = SystemRandom()
33
- return f"{element}_{safe_random.randint(start, end)}"
33
+ return f"{element}_{_SAFE_RANDOM.randint(start, end)}"
34
34
 
35
35
 
36
36
  def get_unique_id(element: str) -> str:
@@ -72,7 +72,7 @@ ELEMENT = Union[
72
72
  ]
73
73
 
74
74
 
75
- def get_xml_type(val: ELEMENT) -> str:
75
+ def get_xml_type(val: Any) -> str:
76
76
  """
77
77
  Get the XML type of a given value.
78
78
 
@@ -227,6 +227,7 @@ def get_xpath31_tag_name(val: Any) -> str:
227
227
  return "string"
228
228
 
229
229
 
230
+ # @lat: [[behavior#XPath 3.1 format]]
230
231
  def convert_to_xpath31(obj: Any, parent_key: str | None = None) -> str:
231
232
  """
232
233
  Convert a Python object to XPath 3.1 json-to-xml format.
@@ -267,7 +268,7 @@ def convert_to_xpath31(obj: Any, parent_key: str | None = None) -> str:
267
268
 
268
269
 
269
270
  def convert(
270
- obj: ELEMENT,
271
+ obj: Any,
271
272
  ids: Any,
272
273
  attr_type: bool,
273
274
  item_func: Callable[[str], str],
@@ -340,7 +341,6 @@ def dict2xml_str(
340
341
  parse dict2xml
341
342
  """
342
343
  ids: list[str] = [] # initialize list of unique ids
343
- ", ".join(str(key) for key in item)
344
344
  subtree = "" # Initialize subtree with default empty string
345
345
 
346
346
  if attr_type:
@@ -631,6 +631,7 @@ def convert_none(
631
631
  return f"<{key}{attr_string}></{key}>"
632
632
 
633
633
 
634
+ # @lat: [[architecture#Conversion engine]]
634
635
  def dicttoxml(
635
636
  obj: ELEMENT,
636
637
  root: bool = True,
@@ -640,7 +641,7 @@ def dicttoxml(
640
641
  item_wrap: bool = True,
641
642
  item_func: Callable[[str], str] = default_item_func,
642
643
  cdata: bool = False,
643
- xml_namespaces: dict[str, Any] = {},
644
+ xml_namespaces: dict[str, Any] | None = None,
644
645
  list_headers: bool = False,
645
646
  xpath_format: bool = False,
646
647
  ) -> bytes:
@@ -796,6 +797,8 @@ def dicttoxml(
796
797
 
797
798
  output = []
798
799
  namespace_str = ""
800
+ if xml_namespaces is None:
801
+ xml_namespaces = {}
799
802
  for prefix in xml_namespaces:
800
803
  if prefix == 'xsi':
801
804
  for schema_att in xml_namespaces[prefix]:
@@ -17,22 +17,24 @@ import logging
17
17
  from collections.abc import Callable
18
18
  from typing import Any
19
19
 
20
+ RustStringTransform = Callable[[str], str]
21
+
20
22
  LOG = logging.getLogger("dicttoxml_fast")
21
23
 
22
24
  # Try to import the Rust implementation
23
25
  _USE_RUST = False
24
- _rust_dicttoxml = None
26
+ _rust_dicttoxml: Callable[..., bytes] | None = None
27
+ rust_escape_xml: RustStringTransform | None = None
28
+ rust_wrap_cdata: RustStringTransform | None = None
25
29
 
26
30
  try:
27
- from json2xml_rs import dicttoxml as _rust_dicttoxml # type: ignore[import-not-found] # pragma: no cover
28
- from json2xml_rs import escape_xml_py as rust_escape_xml # type: ignore[import-not-found] # pragma: no cover
29
- from json2xml_rs import wrap_cdata_py as rust_wrap_cdata # type: ignore[import-not-found] # pragma: no cover
31
+ from json2xml_rs import dicttoxml as _rust_dicttoxml # pragma: no cover
32
+ from json2xml_rs import escape_xml_py as rust_escape_xml # pragma: no cover
33
+ from json2xml_rs import wrap_cdata_py as rust_wrap_cdata # pragma: no cover
30
34
  _USE_RUST = True # pragma: no cover
31
35
  LOG.debug("Using Rust backend for dicttoxml") # pragma: no cover
32
36
  except ImportError: # pragma: no cover
33
37
  LOG.debug("Rust backend not available, using pure Python")
34
- rust_escape_xml = None
35
- rust_wrap_cdata = None
36
38
 
37
39
  # Import the pure Python implementation as fallback
38
40
  from json2xml import dicttoxml as _py_dicttoxml # noqa: E402
@@ -48,6 +50,7 @@ def get_backend() -> str:
48
50
  return "rust" if _USE_RUST else "python"
49
51
 
50
52
 
53
+ # @lat: [[architecture#Backend selection]]
51
54
  def dicttoxml(
52
55
  obj: Any,
53
56
  root: bool = True,
@@ -8,6 +8,7 @@ from json2xml import dicttoxml
8
8
  from .utils import InvalidDataError
9
9
 
10
10
 
11
+ # @lat: [[architecture#Core pipeline]]
11
12
  class Json2xml:
12
13
  """
13
14
  Wrapper class to convert the data to xml
@@ -34,6 +35,8 @@ class Json2xml:
34
35
  self.cdata = cdata
35
36
  self.list_headers = list_headers
36
37
 
38
+ # @lat: [[behavior#Conversion output]]
39
+ # @lat: [[behavior#Invalid XML payloads]]
37
40
  def to_xml(self) -> Any | None:
38
41
  """
39
42
  Convert to xml using dicttoxml.dicttoxml and then pretty print it.
@@ -24,6 +24,7 @@ class StringReadError(Exception):
24
24
  pass
25
25
 
26
26
 
27
+ # @lat: [[behavior#Input readers]]
27
28
  def readfromjson(filename: str) -> dict[str, str]:
28
29
  """Reads a JSON file and returns a dictionary."""
29
30
  try:
@@ -44,7 +45,7 @@ def readfromurl(url: str, params: dict[str, str] | None = None) -> dict[str, str
44
45
  raise URLReadError("URL is not returning correct response")
45
46
 
46
47
 
47
- def readfromstring(jsondata: str) -> dict[str, str]:
48
+ def readfromstring(jsondata: object) -> dict[str, str]:
48
49
  """Loads JSON data from a string and returns a dictionary."""
49
50
  if not isinstance(jsondata, str):
50
51
  raise StringReadError("Input is not a proper JSON string")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: json2xml
3
- Version: 6.0.4
3
+ Version: 6.0.6
4
4
  Summary: Simple Python Library to convert JSON to XML
5
5
  Author-email: Vinit Kumar <mail@vinitkumar.me>
6
6
  License: Apache Software License 2.0
@@ -79,7 +79,7 @@ Installation
79
79
 
80
80
  pip install json2xml
81
81
 
82
- **With Native Rust Acceleration (28x faster)**
82
+ **With Native Rust Acceleration (up to 149x faster)**
83
83
 
84
84
  For maximum performance, install the optional Rust extension:
85
85
 
@@ -91,7 +91,7 @@ For maximum performance, install the optional Rust extension:
91
91
  # Or install the Rust extension separately
92
92
  pip install json2xml-rs
93
93
 
94
- The Rust extension provides **28x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
94
+ The Rust extension provides **75-149x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
95
95
 
96
96
  **As a CLI Tool**
97
97
 
@@ -337,7 +337,7 @@ Using tools directly:
337
337
 
338
338
  **Rust Extension Development**
339
339
 
340
- The optional Rust extension (``json2xml-rs``) provides 29x faster performance. To develop or build the Rust extension:
340
+ The optional Rust extension (``json2xml-rs``) provides up to 149x faster performance. To develop or build the Rust extension:
341
341
 
342
342
  Prerequisites:
343
343
 
@@ -464,21 +464,21 @@ For users who need maximum performance within Python, json2xml includes an optio
464
464
  - Rust Extension
465
465
  - Speedup
466
466
  * - **Small JSON** (47 bytes)
467
- - 40µs
468
- - 1.5µs
469
- - **27x**
467
+ - 78µs
468
+ - 1.05µs
469
+ - **75x**
470
470
  * - **Medium JSON** (3.2 KB)
471
- - 2.1ms
472
- - 71µs
473
- - **30x**
471
+ - 2.15ms
472
+ - 15µs
473
+ - **139x**
474
474
  * - **Large JSON** (32 KB)
475
- - 21ms
476
- - 740µs
477
- - **28x**
475
+ - 22ms
476
+ - 151µs
477
+ - **146x**
478
478
  * - **Very Large JSON** (323 KB)
479
- - 213ms
480
- - 7.5ms
481
- - **28x**
479
+ - 219ms
480
+ - 1.47ms
481
+ - **149x**
482
482
 
483
483
  **Usage with Rust Extension:**
484
484
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "json2xml"
7
- version = "6.0.4"
7
+ version = "6.0.6"
8
8
  description = "Simple Python Library to convert JSON to XML"
9
9
  readme = "README.rst"
10
10
  requires-python = ">=3.10"
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import json
5
5
  from pathlib import Path
6
- from typing import TYPE_CHECKING, Any, Dict, List
6
+ from typing import TYPE_CHECKING, Any
7
7
 
8
8
  import pytest
9
9
 
@@ -588,6 +588,7 @@ class TestCLIUnitTests:
588
588
  assert result == {"key": "value"}
589
589
  mock_stdin.assert_called_once()
590
590
 
591
+ # @lat: [[tests#CLI input resolution#URL input takes priority]]
591
592
  def test_read_input_priority_url_over_string(self) -> None:
592
593
  """Test URL input takes priority over string input."""
593
594
  with patch("json2xml.cli.readfromurl") as mock_url:
@@ -649,6 +650,7 @@ class TestCLIUnitTests:
649
650
  content = output_file.read_text()
650
651
  assert "<key" in content
651
652
 
653
+ # @lat: [[tests#CLI input resolution#Dash argument reads stdin]]
652
654
  def test_read_input_stdin_dash_argument(self) -> None:
653
655
  """Test read_input with '-' as input_file reads from stdin."""
654
656
  with patch("json2xml.cli.read_from_stdin") as mock_stdin:
@@ -3,6 +3,7 @@ import numbers
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
5
  import pytest
6
+ from _pytest.monkeypatch import MonkeyPatch
6
7
 
7
8
  from json2xml import dicttoxml
8
9
 
@@ -10,7 +11,6 @@ if TYPE_CHECKING:
10
11
  from _pytest.capture import CaptureFixture
11
12
  from _pytest.fixtures import FixtureRequest
12
13
  from _pytest.logging import LogCaptureFixture
13
- from _pytest.monkeypatch import MonkeyPatch
14
14
 
15
15
 
16
16
  class TestDict2xml:
@@ -102,6 +102,7 @@ class TestDict2xml:
102
102
  )
103
103
  assert result == b"<bike><item>blue</item><item>green</item></bike>"
104
104
 
105
+ # @lat: [[tests#Conversion behavior#Item-wrap false repeats parent tag]]
105
106
  def test_item_wrap_false(self) -> None:
106
107
  """Test dicttoxml with item_wrap=False."""
107
108
  data = {"bike": ["blue", "green"]}
@@ -499,6 +500,92 @@ class TestDict2xml:
499
500
  result = dicttoxml.dicttoxml(data, xml_namespaces=namespaces)
500
501
  assert b'xmlns="http://example.com"' in result
501
502
 
503
+ # @lat: [[tests#Conversion behavior#Default xml namespaces stay empty]]
504
+ def test_dicttoxml_without_xml_namespaces_keeps_previous_output(self) -> None:
505
+ """Test dicttoxml without xml_namespaces keeps the default XML shape."""
506
+ data = {"bike": "blue"}
507
+ result = dicttoxml.dicttoxml(data, attr_type=False)
508
+ assert (
509
+ b'<?xml version="1.0" encoding="UTF-8" ?>'
510
+ b"<root><bike>blue</bike></root>" == result
511
+ )
512
+ assert b"xmlns" not in result
513
+ assert b"xsi:" not in result
514
+
515
+ # @lat: [[tests#Conversion behavior#Explicit xml namespaces emit schema attributes]]
516
+ def test_dicttoxml_with_explicit_xml_namespaces_emits_schema_attributes(self) -> None:
517
+ """Test dicttoxml emits explicit namespace declarations and XSI schema attributes."""
518
+ data = {"bike": "blue"}
519
+ namespaces = {
520
+ "veh": "https://example.com/vehicle",
521
+ "xsi": {
522
+ "schemaInstance": "http://www.w3.org/2001/XMLSchema-instance",
523
+ "schemaLocation": "https://example.com/vehicle vehicle.xsd",
524
+ "noNamespaceSchemaLocation": "vehicle-no-namespace.xsd",
525
+ },
526
+ }
527
+ namespaces_before = {
528
+ prefix: value.copy() if isinstance(value, dict) else value
529
+ for prefix, value in namespaces.items()
530
+ }
531
+
532
+ result = dicttoxml.dicttoxml(
533
+ data,
534
+ custom_root="vehicle",
535
+ attr_type=False,
536
+ xml_namespaces=namespaces,
537
+ )
538
+
539
+ assert (
540
+ b'<?xml version="1.0" encoding="UTF-8" ?>'
541
+ b'<vehicle xmlns:veh="https://example.com/vehicle" '
542
+ b'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
543
+ b'xsi:schemaLocation="https://example.com/vehicle vehicle.xsd">'
544
+ b"<bike>blue</bike>"
545
+ b"</vehicle>" == result
546
+ )
547
+ assert b"xsi:noNamespaceSchemaLocation" not in result
548
+ assert namespaces == namespaces_before
549
+
550
+ # @lat: [[tests#Conversion behavior#Xml namespace inputs are not mutated across calls]]
551
+ def test_dicttoxml_reuses_xml_namespaces_without_mutating_input(self) -> None:
552
+ """Test reusing xml_namespaces across calls does not mutate or accumulate state."""
553
+ data = {"bike": "blue"}
554
+ namespaces = {
555
+ "veh": "https://example.com/vehicle",
556
+ "xsi": {
557
+ "schemaInstance": "http://www.w3.org/2001/XMLSchema-instance",
558
+ "schemaLocation": "https://example.com/vehicle vehicle.xsd",
559
+ },
560
+ }
561
+ namespaces_before = {
562
+ prefix: value.copy() if isinstance(value, dict) else value
563
+ for prefix, value in namespaces.items()
564
+ }
565
+
566
+ first = dicttoxml.dicttoxml(
567
+ data,
568
+ custom_root="vehicle",
569
+ attr_type=False,
570
+ xml_namespaces=namespaces,
571
+ )
572
+ second = dicttoxml.dicttoxml(
573
+ data,
574
+ custom_root="vehicle",
575
+ attr_type=False,
576
+ xml_namespaces=namespaces,
577
+ )
578
+
579
+ assert first == second
580
+ assert first.count(b'xmlns:veh="https://example.com/vehicle"') == 1
581
+ assert first.count(
582
+ b'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
583
+ ) == 1
584
+ assert first.count(
585
+ b'xsi:schemaLocation="https://example.com/vehicle vehicle.xsd"'
586
+ ) == 1
587
+ assert namespaces == namespaces_before
588
+
502
589
  def test_datetime_conversion(self) -> None:
503
590
  """Test datetime conversion."""
504
591
  data = {"key": datetime.datetime(2023, 2, 15, 12, 30, 45)}
@@ -773,29 +860,20 @@ class TestDict2xml:
773
860
  result = dicttoxml.dicttoxml(data, cdata=True, attr_type=False, root=False)
774
861
  assert b"<key><![CDATA[value]]></key>" == result
775
862
 
776
- def test_get_unique_id_with_duplicates(self) -> None:
863
+ def test_get_unique_id_with_duplicates(self, monkeypatch: MonkeyPatch) -> None:
777
864
  """Test get_unique_id when duplicates are generated."""
778
- # We need to modify the original get_unique_id to simulate a pre-existing ID list
779
865
  import json2xml.dicttoxml as module
780
866
 
781
- # Save original function
782
- original_get_unique_id = module.get_unique_id
783
-
784
- # Track make_id calls
785
867
  call_count = 0
786
- original_make_id = module.make_id
787
868
 
788
869
  def mock_make_id(element: str, start: int = 100000, end: int = 999999) -> str:
789
870
  nonlocal call_count
790
871
  call_count += 1
791
872
  if call_count == 1:
792
- return "test_123456" # First call - will collide
793
- else:
794
- return "test_789012" # Second call - unique
873
+ return "test_123456"
874
+ return "test_789012"
795
875
 
796
- # Patch get_unique_id to use a pre-populated ids list
797
876
  def patched_get_unique_id(element: str) -> str:
798
- # Start with a pre-existing ID to force collision
799
877
  ids = ["test_123456"]
800
878
  this_id = module.make_id(element)
801
879
  dup = True
@@ -804,19 +882,15 @@ class TestDict2xml:
804
882
  dup = False
805
883
  ids.append(this_id)
806
884
  else:
807
- this_id = module.make_id(element) # This exercises line 52
885
+ this_id = module.make_id(element)
808
886
  return ids[-1]
809
887
 
810
- module.make_id = mock_make_id # type: ignore[assignment]
811
- module.get_unique_id = patched_get_unique_id # type: ignore[assignment]
888
+ monkeypatch.setattr(module, "make_id", mock_make_id)
889
+ monkeypatch.setattr(module, "get_unique_id", patched_get_unique_id)
812
890
 
813
- try:
814
- result = dicttoxml.get_unique_id("test")
815
- assert result == "test_789012"
816
- assert call_count == 2
817
- finally:
818
- module.make_id = original_make_id
819
- module.get_unique_id = original_get_unique_id
891
+ result = dicttoxml.get_unique_id("test")
892
+ assert result == "test_789012"
893
+ assert call_count == 2
820
894
 
821
895
  def test_convert_with_bool_direct(self) -> None:
822
896
  """Test convert function with boolean input directly."""
@@ -1011,12 +1085,10 @@ class TestDict2xml:
1011
1085
  )
1012
1086
  assert "<item><item>nested</item><item>list</item></item>" == result
1013
1087
 
1014
- def test_dict2xml_str_with_primitive_dict_rawitem(self) -> None:
1088
+ def test_dict2xml_str_with_primitive_dict_rawitem(self, monkeypatch: MonkeyPatch) -> None:
1015
1089
  """Test dict2xml_str with primitive dict as rawitem to trigger line 274."""
1016
- # Create a case where rawitem is a dict and is_primitive_type returns True
1017
- # This is tricky because normally dicts are not primitive types
1018
- # We need to mock is_primitive_type to return True for a dict
1019
1090
  import json2xml.dicttoxml as module
1091
+
1020
1092
  original_is_primitive = module.is_primitive_type
1021
1093
 
1022
1094
  def mock_is_primitive(val: Any) -> bool:
@@ -1024,22 +1096,19 @@ class TestDict2xml:
1024
1096
  return True
1025
1097
  return original_is_primitive(val)
1026
1098
 
1027
- module.is_primitive_type = mock_is_primitive # type: ignore[assignment]
1028
- try:
1029
- item = {"@val": {"test": "data"}}
1030
- result = dicttoxml.dict2xml_str(
1031
- attr_type=False,
1032
- attr={},
1033
- item=item,
1034
- item_func=lambda x: "item",
1035
- cdata=False,
1036
- item_name="test",
1037
- item_wrap=False,
1038
- parentIsList=False
1039
- )
1040
- assert "test" in result
1041
- finally:
1042
- module.is_primitive_type = original_is_primitive
1099
+ monkeypatch.setattr(module, "is_primitive_type", mock_is_primitive)
1100
+ item = {"@val": {"test": "data"}}
1101
+ result = dicttoxml.dict2xml_str(
1102
+ attr_type=False,
1103
+ attr={},
1104
+ item=item,
1105
+ item_func=lambda x: "item",
1106
+ cdata=False,
1107
+ item_name="test",
1108
+ item_wrap=False,
1109
+ parentIsList=False
1110
+ )
1111
+ assert "test" in result
1043
1112
 
1044
1113
  def test_convert_dict_with_falsy_value_line_400(self) -> None:
1045
1114
  """Test convert_dict with falsy value to trigger line 400."""
@@ -21,12 +21,6 @@ from json2xml.utils import (
21
21
  class TestJson2xml:
22
22
  """Tests for `json2xml` package."""
23
23
 
24
- def setUp(self) -> None:
25
- """Set up test fixtures, if any."""
26
-
27
- def tearDown(self) -> None:
28
- """Tear down test fixtures, if any."""
29
-
30
24
  def test_read_from_json(self) -> None:
31
25
  """Test something."""
32
26
  data = readfromjson("examples/bigexample.json")
@@ -229,6 +223,7 @@ class TestJson2xml:
229
223
  if xmldata:
230
224
  assert b'encoding="UTF-8"' in xmldata
231
225
 
226
+ # @lat: [[tests#Conversion behavior#XPath format adds functions namespace]]
232
227
  def test_xpath_format_basic(self) -> None:
233
228
  """Test XPath 3.1 json-to-xml format with basic types."""
234
229
  data = {"name": "John", "age": 30, "active": True}
@@ -6,6 +6,8 @@ and matches the Python implementation for supported features.
6
6
  """
7
7
  from __future__ import annotations
8
8
 
9
+ from typing import Any
10
+
9
11
  import pytest
10
12
 
11
13
  # Check if Rust extension is available
@@ -250,7 +252,8 @@ class TestRustDicttoxmlOptions:
250
252
  def test_list_headers(self):
251
253
  data = {"colors": ["red", "green"]}
252
254
  result = rust_dicttoxml(data, list_headers=True)
253
- assert b"<colors" in result
255
+ assert b"<item type=\"str\">red</item>" in result
256
+ assert b"<item type=\"str\">green</item>" in result
254
257
 
255
258
 
256
259
  class TestRustVsPythonCompatibility:
@@ -332,21 +335,18 @@ class TestRustVsPythonCompatibility:
332
335
  rust, python = self.compare_outputs(data, item_wrap=False)
333
336
  assert rust == python
334
337
 
335
- @pytest.mark.xfail(reason="Rust list_headers implementation differs from Python - uses different wrapping semantics")
336
338
  def test_list_headers_true_matches(self):
337
339
  """Test that list_headers=True produces matching output."""
338
340
  data = {"items": ["one", "two", "three"]}
339
341
  rust, python = self.compare_outputs(data, list_headers=True)
340
342
  assert rust == python
341
343
 
342
- @pytest.mark.xfail(reason="Rust item_wrap=False with nested dicts differs from Python - known limitation")
343
344
  def test_item_wrap_false_with_nested_dict_matches(self):
344
345
  """Test item_wrap=False with nested dicts in list."""
345
346
  data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
346
347
  rust, python = self.compare_outputs(data, item_wrap=False)
347
348
  assert rust == python
348
349
 
349
- @pytest.mark.xfail(reason="Rust list_headers with nested structures differs from Python - known limitation")
350
350
  def test_list_headers_with_nested_matches(self):
351
351
  """Test list_headers=True with nested structures."""
352
352
  data = {"products": [{"id": 1, "name": "Widget"}, {"id": 2, "name": "Gadget"}]}
@@ -494,8 +494,8 @@ class TestRustPerformanceBasics:
494
494
 
495
495
  def test_deeply_nested_structure(self):
496
496
  """Ensure deep nesting doesn't crash."""
497
- data = {"level": None}
498
- current = data
497
+ data: dict[str, Any] = {"level": None}
498
+ current: dict[str, Any] = data
499
499
  for i in range(50):
500
500
  current["level"] = {"value": i}
501
501
  current = current["level"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes