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.
- {json2xml-6.0.4 → json2xml-6.0.6}/HISTORY.rst +23 -1
- {json2xml-6.0.4 → json2xml-6.0.6}/PKG-INFO +16 -16
- {json2xml-6.0.4 → json2xml-6.0.6}/README.rst +15 -15
- {json2xml-6.0.4 → json2xml-6.0.6}/json2xml/__init__.py +1 -2
- {json2xml-6.0.4 → json2xml-6.0.6}/json2xml/cli.py +2 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/json2xml/dicttoxml.py +9 -6
- {json2xml-6.0.4 → json2xml-6.0.6}/json2xml/dicttoxml_fast.py +9 -6
- {json2xml-6.0.4 → json2xml-6.0.6}/json2xml/json2xml.py +3 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/json2xml/utils.py +2 -1
- {json2xml-6.0.4 → json2xml-6.0.6}/json2xml.egg-info/PKG-INFO +16 -16
- {json2xml-6.0.4 → json2xml-6.0.6}/pyproject.toml +1 -1
- {json2xml-6.0.4 → json2xml-6.0.6}/tests/conftest.py +1 -1
- {json2xml-6.0.4 → json2xml-6.0.6}/tests/test_cli.py +2 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/tests/test_dict2xml.py +112 -43
- {json2xml-6.0.4 → json2xml-6.0.6}/tests/test_json2xml.py +1 -6
- {json2xml-6.0.4 → json2xml-6.0.6}/tests/test_rust_dicttoxml.py +6 -6
- {json2xml-6.0.4 → json2xml-6.0.6}/AUTHORS.rst +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/CONTRIBUTING.rst +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/LICENSE +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/MANIFEST.in +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/Makefile +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/authors.rst +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/benchmarks.rst +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/conf.py +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/contributing.rst +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/history.rst +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/index.rst +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/installation.rst +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/json2xml.rst +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/make.bat +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/modules.rst +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/readme.rst +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/docs/usage.rst +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/json2xml.egg-info/SOURCES.txt +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/json2xml.egg-info/dependency_links.txt +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/json2xml.egg-info/entry_points.txt +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/json2xml.egg-info/requires.txt +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/json2xml.egg-info/top_level.txt +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/requirements.in +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/setup.cfg +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/setup.py +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/tests/__init__.py +0 -0
- {json2xml-6.0.4 → json2xml-6.0.6}/tests/test_missing_coverage.py +0 -0
- {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.
|
|
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 (
|
|
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 **
|
|
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
|
|
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
|
-
-
|
|
468
|
-
- 1.
|
|
469
|
-
- **
|
|
467
|
+
- 78µs
|
|
468
|
+
- 1.05µs
|
|
469
|
+
- **75x**
|
|
470
470
|
* - **Medium JSON** (3.2 KB)
|
|
471
|
-
- 2.
|
|
472
|
-
-
|
|
473
|
-
- **
|
|
471
|
+
- 2.15ms
|
|
472
|
+
- 15µs
|
|
473
|
+
- **139x**
|
|
474
474
|
* - **Large JSON** (32 KB)
|
|
475
|
-
-
|
|
476
|
-
-
|
|
477
|
-
- **
|
|
475
|
+
- 22ms
|
|
476
|
+
- 151µs
|
|
477
|
+
- **146x**
|
|
478
478
|
* - **Very Large JSON** (323 KB)
|
|
479
|
-
-
|
|
480
|
-
-
|
|
481
|
-
- **
|
|
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 (
|
|
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 **
|
|
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
|
|
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
|
-
-
|
|
432
|
-
- 1.
|
|
433
|
-
- **
|
|
431
|
+
- 78µs
|
|
432
|
+
- 1.05µs
|
|
433
|
+
- **75x**
|
|
434
434
|
* - **Medium JSON** (3.2 KB)
|
|
435
|
-
- 2.
|
|
436
|
-
-
|
|
437
|
-
- **
|
|
435
|
+
- 2.15ms
|
|
436
|
+
- 15µs
|
|
437
|
+
- **139x**
|
|
438
438
|
* - **Large JSON** (32 KB)
|
|
439
|
-
-
|
|
440
|
-
-
|
|
441
|
-
- **
|
|
439
|
+
- 22ms
|
|
440
|
+
- 151µs
|
|
441
|
+
- **146x**
|
|
442
442
|
* - **Very Large JSON** (323 KB)
|
|
443
|
-
-
|
|
444
|
-
-
|
|
445
|
-
- **
|
|
443
|
+
- 219ms
|
|
444
|
+
- 1.47ms
|
|
445
|
+
- **149x**
|
|
446
446
|
|
|
447
447
|
**Usage with Rust Extension:**
|
|
448
448
|
|
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
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 #
|
|
28
|
-
from json2xml_rs import escape_xml_py as rust_escape_xml #
|
|
29
|
-
from json2xml_rs import wrap_cdata_py as rust_wrap_cdata #
|
|
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:
|
|
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.
|
|
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 (
|
|
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 **
|
|
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
|
|
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
|
-
-
|
|
468
|
-
- 1.
|
|
469
|
-
- **
|
|
467
|
+
- 78µs
|
|
468
|
+
- 1.05µs
|
|
469
|
+
- **75x**
|
|
470
470
|
* - **Medium JSON** (3.2 KB)
|
|
471
|
-
- 2.
|
|
472
|
-
-
|
|
473
|
-
- **
|
|
471
|
+
- 2.15ms
|
|
472
|
+
- 15µs
|
|
473
|
+
- **139x**
|
|
474
474
|
* - **Large JSON** (32 KB)
|
|
475
|
-
-
|
|
476
|
-
-
|
|
477
|
-
- **
|
|
475
|
+
- 22ms
|
|
476
|
+
- 151µs
|
|
477
|
+
- **146x**
|
|
478
478
|
* - **Very Large JSON** (323 KB)
|
|
479
|
-
-
|
|
480
|
-
-
|
|
481
|
-
- **
|
|
479
|
+
- 219ms
|
|
480
|
+
- 1.47ms
|
|
481
|
+
- **149x**
|
|
482
482
|
|
|
483
483
|
**Usage with Rust Extension:**
|
|
484
484
|
|
|
@@ -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"
|
|
793
|
-
|
|
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)
|
|
885
|
+
this_id = module.make_id(element)
|
|
808
886
|
return ids[-1]
|
|
809
887
|
|
|
810
|
-
module
|
|
811
|
-
module
|
|
888
|
+
monkeypatch.setattr(module, "make_id", mock_make_id)
|
|
889
|
+
monkeypatch.setattr(module, "get_unique_id", patched_get_unique_id)
|
|
812
890
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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"<
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|