json2xml 6.0.5__tar.gz → 6.0.7__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 (46) hide show
  1. {json2xml-6.0.5 → json2xml-6.0.7}/HISTORY.rst +17 -0
  2. {json2xml-6.0.5 → json2xml-6.0.7}/PKG-INFO +45 -45
  3. {json2xml-6.0.5 → json2xml-6.0.7}/README.rst +44 -44
  4. {json2xml-6.0.5 → json2xml-6.0.7}/json2xml/__init__.py +1 -2
  5. {json2xml-6.0.5 → json2xml-6.0.7}/json2xml/cli.py +18 -15
  6. {json2xml-6.0.5 → json2xml-6.0.7}/json2xml/dicttoxml.py +34 -30
  7. {json2xml-6.0.5 → json2xml-6.0.7}/json2xml/dicttoxml_fast.py +9 -6
  8. {json2xml-6.0.5 → json2xml-6.0.7}/json2xml/json2xml.py +6 -2
  9. json2xml-6.0.7/json2xml/types.py +8 -0
  10. json2xml-6.0.7/json2xml/utils.py +73 -0
  11. {json2xml-6.0.5 → json2xml-6.0.7}/json2xml.egg-info/PKG-INFO +45 -45
  12. {json2xml-6.0.5 → json2xml-6.0.7}/json2xml.egg-info/SOURCES.txt +1 -0
  13. {json2xml-6.0.5 → json2xml-6.0.7}/pyproject.toml +1 -2
  14. {json2xml-6.0.5 → json2xml-6.0.7}/setup.cfg +1 -1
  15. {json2xml-6.0.5 → json2xml-6.0.7}/tests/conftest.py +1 -1
  16. {json2xml-6.0.5 → json2xml-6.0.7}/tests/test_cli.py +2 -0
  17. {json2xml-6.0.5 → json2xml-6.0.7}/tests/test_dict2xml.py +183 -44
  18. {json2xml-6.0.5 → json2xml-6.0.7}/tests/test_json2xml.py +18 -6
  19. {json2xml-6.0.5 → json2xml-6.0.7}/tests/test_rust_dicttoxml.py +23 -4
  20. {json2xml-6.0.5 → json2xml-6.0.7}/tests/test_utils.py +66 -79
  21. json2xml-6.0.5/json2xml/utils.py +0 -54
  22. {json2xml-6.0.5 → json2xml-6.0.7}/AUTHORS.rst +0 -0
  23. {json2xml-6.0.5 → json2xml-6.0.7}/CONTRIBUTING.rst +0 -0
  24. {json2xml-6.0.5 → json2xml-6.0.7}/LICENSE +0 -0
  25. {json2xml-6.0.5 → json2xml-6.0.7}/MANIFEST.in +0 -0
  26. {json2xml-6.0.5 → json2xml-6.0.7}/docs/Makefile +0 -0
  27. {json2xml-6.0.5 → json2xml-6.0.7}/docs/authors.rst +0 -0
  28. {json2xml-6.0.5 → json2xml-6.0.7}/docs/benchmarks.rst +0 -0
  29. {json2xml-6.0.5 → json2xml-6.0.7}/docs/conf.py +0 -0
  30. {json2xml-6.0.5 → json2xml-6.0.7}/docs/contributing.rst +0 -0
  31. {json2xml-6.0.5 → json2xml-6.0.7}/docs/history.rst +0 -0
  32. {json2xml-6.0.5 → json2xml-6.0.7}/docs/index.rst +0 -0
  33. {json2xml-6.0.5 → json2xml-6.0.7}/docs/installation.rst +0 -0
  34. {json2xml-6.0.5 → json2xml-6.0.7}/docs/json2xml.rst +0 -0
  35. {json2xml-6.0.5 → json2xml-6.0.7}/docs/make.bat +0 -0
  36. {json2xml-6.0.5 → json2xml-6.0.7}/docs/modules.rst +0 -0
  37. {json2xml-6.0.5 → json2xml-6.0.7}/docs/readme.rst +0 -0
  38. {json2xml-6.0.5 → json2xml-6.0.7}/docs/usage.rst +0 -0
  39. {json2xml-6.0.5 → json2xml-6.0.7}/json2xml.egg-info/dependency_links.txt +0 -0
  40. {json2xml-6.0.5 → json2xml-6.0.7}/json2xml.egg-info/entry_points.txt +0 -0
  41. {json2xml-6.0.5 → json2xml-6.0.7}/json2xml.egg-info/requires.txt +0 -0
  42. {json2xml-6.0.5 → json2xml-6.0.7}/json2xml.egg-info/top_level.txt +0 -0
  43. {json2xml-6.0.5 → json2xml-6.0.7}/requirements.in +0 -0
  44. {json2xml-6.0.5 → json2xml-6.0.7}/setup.py +0 -0
  45. {json2xml-6.0.5 → json2xml-6.0.7}/tests/__init__.py +0 -0
  46. {json2xml-6.0.5 → json2xml-6.0.7}/tests/test_missing_coverage.py +0 -0
@@ -1,4 +1,21 @@
1
1
 
2
+ 6.0.7 / 2026-04-26
3
+ ==================
4
+
5
+ * fix: handle JSON conversion edge cases and improve URL reader robustness (#284)
6
+ * chore(deps-dev): bump pytest from 9.0.1 to 9.0.3 (#282)
7
+
8
+
9
+ 6.0.6 / 2026-04-08
10
+ ==================
11
+
12
+ * fix: review cleanup (#281)
13
+ * fix: remove needless line (#280)
14
+ * docs: add lat.md support to the codebase (#279)
15
+ * chore(deps): bump pygments from 2.19.1 to 2.20.0 in /docs (#278)
16
+ * fix: upgrade Python workflow to 3.12
17
+
18
+
2
19
  6.0.5 / 2026-03-12
3
20
  ==================
4
21
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: json2xml
3
- Version: 6.0.5
3
+ Version: 6.0.7
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 (up to 149x faster)**
82
+ **With Native Rust Acceleration (up to 129x 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 **75-149x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
94
+ The Rust extension provides **57-129x faster** conversion compared to pure Python in the latest benchmark. 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 up to 149x faster performance. To develop or build the Rust extension:
340
+ The optional Rust extension (``json2xml-rs``) provides up to 129x faster performance in the latest benchmark. 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
- - 78µs
468
- - 1.05µs
469
- - **75x**
467
+ - 31.49µs
468
+ - 0.55µs
469
+ - **56.8x**
470
470
  * - **Medium JSON** (3.2 KB)
471
- - 2.15ms
472
- - 15µs
473
- - **139x**
471
+ - 1.69ms
472
+ - 16.15µs
473
+ - **105.0x**
474
474
  * - **Large JSON** (32 KB)
475
- - 22ms
476
- - 151µs
477
- - **146x**
475
+ - 17.97ms
476
+ - 168.21µs
477
+ - **106.8x**
478
478
  * - **Very Large JSON** (323 KB)
479
- - 219ms
480
- - 1.47ms
481
- - **149x**
479
+ - 183.33ms
480
+ - 1.42ms
481
+ - **129.0x**
482
482
 
483
483
  **Usage with Rust Extension:**
484
484
 
@@ -508,7 +508,7 @@ For other platforms, the pure Python version is used automatically.
508
508
  Performance Benchmarks
509
509
  ^^^^^^^^^^^^^^^^^^^^^^
510
510
 
511
- Comprehensive benchmarks comparing all implementations (Apple Silicon, January 2026):
511
+ Comprehensive benchmarks comparing all implementations (Apple Silicon, macOS 26.4.1, Python 3.14.4, April 2026):
512
512
 
513
513
  .. list-table::
514
514
  :header-rows: 1
@@ -521,40 +521,40 @@ Comprehensive benchmarks comparing all implementations (Apple Silicon, January 2
521
521
  - Zig
522
522
  - Best
523
523
  * - **Small** (47B)
524
- - 40µs
525
- - 1.5µs
526
- - 4.6ms
527
- - 3.7ms
528
- - Rust (28x)
524
+ - 31.49µs
525
+ - 0.55µs
526
+ - 4.09ms
527
+ - 2.02ms
528
+ - Rust (56.8x)
529
529
  * - **Medium** (3.2KB)
530
- - 2.1ms
531
- - 71µs
532
- - 4.1ms
533
- - 3.3ms
534
- - Rust (30x)
530
+ - 1.69ms
531
+ - 16.15µs
532
+ - 4.07ms
533
+ - 2.09ms
534
+ - Rust (105.0x)
535
535
  * - **Large** (32KB)
536
- - 21ms
537
- - 740µs
538
- - 4ms
539
- - 6.1ms
540
- - Rust (28x)
536
+ - 17.97ms
537
+ - 168.21µs
538
+ - 4.10ms
539
+ - 2.42ms
540
+ - Rust (106.8x)
541
541
  * - **Very Large** (323KB)
542
- - 213ms
543
- - 7.5ms
544
- - 4.4ms
545
- - 33ms
546
- - Go (48x)
542
+ - 183.33ms
543
+ - 1.42ms
544
+ - 4.20ms
545
+ - 5.12ms
546
+ - Rust (129.0x)
547
547
 
548
548
  **Key Findings:**
549
549
 
550
- - **Rust extension**: ~28x faster than Python, zero overhead (best for Python users)
551
- - **Go CLI**: 48x faster for large files (300KB+), but has ~4ms startup overhead
552
- - **Zig CLI**: 3-6x faster for medium-large files
550
+ - **Rust extension**: 57-129x faster than Python, zero process overhead (best for Python users)
551
+ - **Go CLI**: 43.6x faster for very large files (300KB+), but has ~4ms startup overhead
552
+ - **Zig CLI**: 7.4x faster for large files and 35.8x faster for very large files, with ~2ms startup overhead
553
553
 
554
554
  **Recommendation by Use Case:**
555
555
 
556
- - **Python library calls**: Use ``pip install json2xml[fast]`` (Rust, 28x faster)
557
- - **Large file CLI processing**: Use `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ (Go, 48x for 300KB+)
556
+ - **Python library calls**: Use ``pip install json2xml[fast]`` (Rust, up to 129x faster)
557
+ - **Large file CLI processing**: Use `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ or `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ depending on your workload
558
558
  - **Pure Python required**: Use ``pip install json2xml``
559
559
 
560
560
  For detailed benchmarks, see `BENCHMARKS.md <https://github.com/vinitkumar/json2xml/blob/master/BENCHMARKS.md>`_.
@@ -565,9 +565,9 @@ Other Implementations
565
565
 
566
566
  This library is also available in other languages:
567
567
 
568
- - **Rust**: `json2xml-rs <https://pypi.org/project/json2xml-rs/>`_ - 28x faster, Python extension via PyO3
569
- - **Go**: `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ - 48x faster for large files, native CLI
570
- - **Zig**: `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ - 6x faster, native CLI
568
+ - **Rust**: `json2xml-rs <https://pypi.org/project/json2xml-rs/>`_ - up to 129x faster, Python extension via PyO3
569
+ - **Go**: `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ - 43.6x faster for very large files, native CLI
570
+ - **Zig**: `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ - 35.8x faster for very large files, native CLI
571
571
 
572
572
 
573
573
  Help and Support to maintain this project
@@ -43,7 +43,7 @@ Installation
43
43
 
44
44
  pip install json2xml
45
45
 
46
- **With Native Rust Acceleration (up to 149x faster)**
46
+ **With Native Rust Acceleration (up to 129x 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 **75-149x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
58
+ The Rust extension provides **57-129x faster** conversion compared to pure Python in the latest benchmark. 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 up to 149x faster performance. To develop or build the Rust extension:
304
+ The optional Rust extension (``json2xml-rs``) provides up to 129x faster performance in the latest benchmark. 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
- - 78µs
432
- - 1.05µs
433
- - **75x**
431
+ - 31.49µs
432
+ - 0.55µs
433
+ - **56.8x**
434
434
  * - **Medium JSON** (3.2 KB)
435
- - 2.15ms
436
- - 15µs
437
- - **139x**
435
+ - 1.69ms
436
+ - 16.15µs
437
+ - **105.0x**
438
438
  * - **Large JSON** (32 KB)
439
- - 22ms
440
- - 151µs
441
- - **146x**
439
+ - 17.97ms
440
+ - 168.21µs
441
+ - **106.8x**
442
442
  * - **Very Large JSON** (323 KB)
443
- - 219ms
444
- - 1.47ms
445
- - **149x**
443
+ - 183.33ms
444
+ - 1.42ms
445
+ - **129.0x**
446
446
 
447
447
  **Usage with Rust Extension:**
448
448
 
@@ -472,7 +472,7 @@ For other platforms, the pure Python version is used automatically.
472
472
  Performance Benchmarks
473
473
  ^^^^^^^^^^^^^^^^^^^^^^
474
474
 
475
- Comprehensive benchmarks comparing all implementations (Apple Silicon, January 2026):
475
+ Comprehensive benchmarks comparing all implementations (Apple Silicon, macOS 26.4.1, Python 3.14.4, April 2026):
476
476
 
477
477
  .. list-table::
478
478
  :header-rows: 1
@@ -485,40 +485,40 @@ Comprehensive benchmarks comparing all implementations (Apple Silicon, January 2
485
485
  - Zig
486
486
  - Best
487
487
  * - **Small** (47B)
488
- - 40µs
489
- - 1.5µs
490
- - 4.6ms
491
- - 3.7ms
492
- - Rust (28x)
488
+ - 31.49µs
489
+ - 0.55µs
490
+ - 4.09ms
491
+ - 2.02ms
492
+ - Rust (56.8x)
493
493
  * - **Medium** (3.2KB)
494
- - 2.1ms
495
- - 71µs
496
- - 4.1ms
497
- - 3.3ms
498
- - Rust (30x)
494
+ - 1.69ms
495
+ - 16.15µs
496
+ - 4.07ms
497
+ - 2.09ms
498
+ - Rust (105.0x)
499
499
  * - **Large** (32KB)
500
- - 21ms
501
- - 740µs
502
- - 4ms
503
- - 6.1ms
504
- - Rust (28x)
500
+ - 17.97ms
501
+ - 168.21µs
502
+ - 4.10ms
503
+ - 2.42ms
504
+ - Rust (106.8x)
505
505
  * - **Very Large** (323KB)
506
- - 213ms
507
- - 7.5ms
508
- - 4.4ms
509
- - 33ms
510
- - Go (48x)
506
+ - 183.33ms
507
+ - 1.42ms
508
+ - 4.20ms
509
+ - 5.12ms
510
+ - Rust (129.0x)
511
511
 
512
512
  **Key Findings:**
513
513
 
514
- - **Rust extension**: ~28x faster than Python, zero overhead (best for Python users)
515
- - **Go CLI**: 48x faster for large files (300KB+), but has ~4ms startup overhead
516
- - **Zig CLI**: 3-6x faster for medium-large files
514
+ - **Rust extension**: 57-129x faster than Python, zero process overhead (best for Python users)
515
+ - **Go CLI**: 43.6x faster for very large files (300KB+), but has ~4ms startup overhead
516
+ - **Zig CLI**: 7.4x faster for large files and 35.8x faster for very large files, with ~2ms startup overhead
517
517
 
518
518
  **Recommendation by Use Case:**
519
519
 
520
- - **Python library calls**: Use ``pip install json2xml[fast]`` (Rust, 28x faster)
521
- - **Large file CLI processing**: Use `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ (Go, 48x for 300KB+)
520
+ - **Python library calls**: Use ``pip install json2xml[fast]`` (Rust, up to 129x faster)
521
+ - **Large file CLI processing**: Use `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ or `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ depending on your workload
522
522
  - **Pure Python required**: Use ``pip install json2xml``
523
523
 
524
524
  For detailed benchmarks, see `BENCHMARKS.md <https://github.com/vinitkumar/json2xml/blob/master/BENCHMARKS.md>`_.
@@ -529,9 +529,9 @@ Other Implementations
529
529
 
530
530
  This library is also available in other languages:
531
531
 
532
- - **Rust**: `json2xml-rs <https://pypi.org/project/json2xml-rs/>`_ - 28x faster, Python extension via PyO3
533
- - **Go**: `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ - 48x faster for large files, native CLI
534
- - **Zig**: `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ - 6x faster, native CLI
532
+ - **Rust**: `json2xml-rs <https://pypi.org/project/json2xml-rs/>`_ - up to 129x faster, Python extension via PyO3
533
+ - **Go**: `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ - 43.6x faster for very large files, native CLI
534
+ - **Zig**: `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ - 35.8x faster for very large files, native CLI
535
535
 
536
536
 
537
537
  Help and Support to maintain this project
@@ -2,5 +2,4 @@
2
2
 
3
3
  __author__ = """Vinit Kumar"""
4
4
  __email__ = "mail@vinitkumar.me"
5
- __version__ = "6.0.5"
6
-
5
+ __version__ = "6.0.7"
@@ -43,10 +43,11 @@ from __future__ import annotations
43
43
 
44
44
  import argparse
45
45
  import sys
46
- from typing import Any
46
+ from typing import NoReturn
47
47
 
48
48
  from json2xml import __version__
49
49
  from json2xml.json2xml import Json2xml
50
+ from json2xml.types import JSONValue
50
51
  from json2xml.utils import (
51
52
  JSONReadError,
52
53
  StringReadError,
@@ -60,6 +61,13 @@ AUTHOR = "Vinit Kumar"
60
61
  EMAIL = "mail@vinitkumar.me"
61
62
 
62
63
 
64
+ def exit_with_error(message: str) -> NoReturn:
65
+ """Print an error message and terminate CLI processing."""
66
+ print(message, file=sys.stderr)
67
+ raise SystemExit(1)
68
+
69
+
70
+ # @lat: [[architecture#CLI entrypoint]]
63
71
  def create_parser() -> argparse.ArgumentParser:
64
72
  """Create and configure the argument parser."""
65
73
  parser = argparse.ArgumentParser(
@@ -228,7 +236,8 @@ Examples:
228
236
  return parser
229
237
 
230
238
 
231
- def read_input(args: argparse.Namespace) -> dict[str, Any] | list[Any]:
239
+ # @lat: [[behavior#Input readers]]
240
+ def read_input(args: argparse.Namespace) -> JSONValue:
232
241
  """
233
242
  Read JSON input from the specified source.
234
243
 
@@ -248,15 +257,13 @@ def read_input(args: argparse.Namespace) -> dict[str, Any] | list[Any]:
248
257
  try:
249
258
  return readfromurl(args.url)
250
259
  except URLReadError as e:
251
- print(f"Error reading from URL: {e}", file=sys.stderr)
252
- sys.exit(1)
260
+ exit_with_error(f"Error reading from URL: {e}")
253
261
 
254
262
  if args.string:
255
263
  try:
256
264
  return readfromstring(args.string)
257
265
  except StringReadError as e:
258
- print(f"Error parsing JSON string: {e}", file=sys.stderr)
259
- sys.exit(1)
266
+ exit_with_error(f"Error parsing JSON string: {e}")
260
267
 
261
268
  if args.input_file:
262
269
  if args.input_file == "-":
@@ -265,18 +272,16 @@ def read_input(args: argparse.Namespace) -> dict[str, Any] | list[Any]:
265
272
  try:
266
273
  return readfromjson(args.input_file)
267
274
  except JSONReadError as e:
268
- print(f"Error reading JSON file: {e}", file=sys.stderr)
269
- sys.exit(1)
275
+ exit_with_error(f"Error reading JSON file: {e}")
270
276
 
271
277
  # Check if there's data on stdin
272
278
  if not sys.stdin.isatty():
273
279
  return read_from_stdin()
274
280
 
275
- print("Error: No input provided. Use -h for help.", file=sys.stderr)
276
- sys.exit(1)
281
+ exit_with_error("Error: No input provided. Use -h for help.")
277
282
 
278
283
 
279
- def read_from_stdin() -> dict[str, Any] | list[Any]:
284
+ def read_from_stdin() -> JSONValue:
280
285
  """
281
286
  Read JSON from standard input.
282
287
 
@@ -289,12 +294,10 @@ def read_from_stdin() -> dict[str, Any] | list[Any]:
289
294
  try:
290
295
  json_str = sys.stdin.read().strip()
291
296
  if not json_str:
292
- print("Error: Empty input", file=sys.stderr)
293
- sys.exit(1)
297
+ exit_with_error("Error: Empty input")
294
298
  return readfromstring(json_str)
295
299
  except StringReadError as e:
296
- print(f"Error parsing JSON from stdin: {e}", file=sys.stderr)
297
- sys.exit(1)
300
+ exit_with_error(f"Error parsing JSON from stdin: {e}")
298
301
 
299
302
 
300
303
  def write_output(output: str | bytes, output_file: str | None) -> None:
@@ -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
 
@@ -102,7 +102,7 @@ def get_xml_type(val: ELEMENT) -> str:
102
102
  return type(val).__name__
103
103
 
104
104
 
105
- def escape_xml(s: str | int | float | numbers.Number) -> str:
105
+ def escape_xml(s: str | int | float | numbers.Number | None) -> str:
106
106
  """
107
107
  Escape a string for use in XML.
108
108
 
@@ -155,32 +155,24 @@ def key_is_valid_xml(key: str) -> bool:
155
155
 
156
156
 
157
157
  def make_valid_xml_name(key: str, attr: dict[str, Any]) -> tuple[str, dict[str, Any]]:
158
- """Tests an XML name and fixes it if invalid"""
159
- key = escape_xml(key)
160
- # nothing happens at escape_xml if attr is not a string, we don't
161
- # need to pass it to the method at all.
162
- # attr = escape_xml(attr)
158
+ """Return a valid XML element name and carry the original key as metadata when needed."""
159
+ key = str(key)
163
160
 
164
- # pass through if key is already valid
165
161
  if key_is_valid_xml(key):
166
162
  return key, attr
167
163
 
168
- # prepend a lowercase n if the key is numeric
169
- if isinstance(key, int) or key.isdigit():
164
+ if key.isdigit():
170
165
  return f"n{key}", attr
171
166
 
172
- # replace spaces with underscores if that fixes the problem
173
- if key_is_valid_xml(key.replace(" ", "_")):
174
- return key.replace(" ", "_"), attr
167
+ key_with_underscores = key.replace(" ", "_")
168
+ if key_is_valid_xml(key_with_underscores):
169
+ return key_with_underscores, attr
175
170
 
176
- # allow namespace prefixes + ignore @flat in key
177
- if key_is_valid_xml(key.replace(":", "").replace("@flat", "")):
171
+ if ":" in key and key_is_valid_xml(key.replace(":", "")):
178
172
  return key, attr
179
173
 
180
- # key is still invalid - move it into a name attribute
181
174
  attr["name"] = key
182
- key = "key"
183
- return key, attr
175
+ return "key", attr
184
176
 
185
177
 
186
178
  def wrap_cdata(s: str | int | float | numbers.Number) -> str:
@@ -227,6 +219,7 @@ def get_xpath31_tag_name(val: Any) -> str:
227
219
  return "string"
228
220
 
229
221
 
222
+ # @lat: [[behavior#XPath 3.1 format]]
230
223
  def convert_to_xpath31(obj: Any, parent_key: str | None = None) -> str:
231
224
  """
232
225
  Convert a Python object to XPath 3.1 json-to-xml format.
@@ -267,7 +260,7 @@ def convert_to_xpath31(obj: Any, parent_key: str | None = None) -> str:
267
260
 
268
261
 
269
262
  def convert(
270
- obj: ELEMENT,
263
+ obj: Any,
271
264
  ids: Any,
272
265
  attr_type: bool,
273
266
  item_func: Callable[[str], str],
@@ -340,18 +333,24 @@ def dict2xml_str(
340
333
  parse dict2xml
341
334
  """
342
335
  ids: list[str] = [] # initialize list of unique ids
343
- ", ".join(str(key) for key in item)
344
336
  subtree = "" # Initialize subtree with default empty string
345
337
 
346
338
  if attr_type:
347
339
  attr["type"] = get_xml_type(item)
348
- val_attr: dict[str, str] = item.pop("@attrs", attr) # update attr with custom @attr if exists
349
- rawitem = item["@val"] if "@val" in item else item
340
+ val_attr = dict(item["@attrs"]) if "@attrs" in item else dict(attr)
341
+ if "@val" in item:
342
+ rawitem = item["@val"]
343
+ elif "@attrs" in item:
344
+ rawitem = {key: value for key, value in item.items() if key != "@attrs"}
345
+ else:
346
+ rawitem = item
350
347
  if is_primitive_type(rawitem):
351
- if isinstance(rawitem, dict):
348
+ if rawitem is None:
349
+ subtree = ""
350
+ elif isinstance(rawitem, bool):
351
+ subtree = str(rawitem).lower()
352
+ else:
352
353
  subtree = escape_xml(str(rawitem))
353
- if isinstance(rawitem, str):
354
- subtree = escape_xml(rawitem)
355
354
  else:
356
355
  # we can not use convert_dict, because rawitem could be non-dict
357
356
  subtree = convert(
@@ -423,8 +422,10 @@ def convert_dict(
423
422
 
424
423
  for key, val in obj.items():
425
424
  attr = {} if not ids else {"id": f"{get_unique_id(parent)}"}
425
+ key_is_flat = isinstance(key, str) and key.endswith("@flat")
426
+ xml_key = key[:-5] if key_is_flat else key
426
427
 
427
- key, attr = make_valid_xml_name(key, attr)
428
+ key, attr = make_valid_xml_name(xml_key, attr)
428
429
 
429
430
  # since bool is also a subtype of number.Number and int, the check for bool
430
431
  # never comes and hence we get wrong value for the xml type bool
@@ -468,7 +469,7 @@ def convert_dict(
468
469
  item=val,
469
470
  item_func=item_func,
470
471
  cdata=cdata,
471
- item_name=key,
472
+ item_name=f"{key}@flat" if key_is_flat else key,
472
473
  item_wrap=item_wrap,
473
474
  list_headers=list_headers
474
475
  )
@@ -631,6 +632,7 @@ def convert_none(
631
632
  return f"<{key}{attr_string}></{key}>"
632
633
 
633
634
 
635
+ # @lat: [[architecture#Conversion engine]]
634
636
  def dicttoxml(
635
637
  obj: ELEMENT,
636
638
  root: bool = True,
@@ -640,7 +642,7 @@ def dicttoxml(
640
642
  item_wrap: bool = True,
641
643
  item_func: Callable[[str], str] = default_item_func,
642
644
  cdata: bool = False,
643
- xml_namespaces: dict[str, Any] = {},
645
+ xml_namespaces: dict[str, Any] | None = None,
644
646
  list_headers: bool = False,
645
647
  xpath_format: bool = False,
646
648
  ) -> bytes:
@@ -796,6 +798,8 @@ def dicttoxml(
796
798
 
797
799
  output = []
798
800
  namespace_str = ""
801
+ if xml_namespaces is None:
802
+ xml_namespaces = {}
799
803
  for prefix in xml_namespaces:
800
804
  if prefix == 'xsi':
801
805
  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,
@@ -5,16 +5,18 @@ from defusedxml.minidom import parseString
5
5
 
6
6
  from json2xml import dicttoxml
7
7
 
8
+ from .types import JSONValue
8
9
  from .utils import InvalidDataError
9
10
 
10
11
 
12
+ # @lat: [[architecture#Core pipeline]]
11
13
  class Json2xml:
12
14
  """
13
15
  Wrapper class to convert the data to xml
14
16
  """
15
17
  def __init__(
16
18
  self,
17
- data: dict[str, Any] | list[Any] | None = None,
19
+ data: JSONValue = None,
18
20
  wrapper: str = "all",
19
21
  root: bool = True,
20
22
  pretty: bool = True,
@@ -34,11 +36,13 @@ class Json2xml:
34
36
  self.cdata = cdata
35
37
  self.list_headers = list_headers
36
38
 
39
+ # @lat: [[behavior#Conversion output]]
40
+ # @lat: [[behavior#Invalid XML payloads]]
37
41
  def to_xml(self) -> Any | None:
38
42
  """
39
43
  Convert to xml using dicttoxml.dicttoxml and then pretty print it.
40
44
  """
41
- if self.data:
45
+ if self.data is not None:
42
46
  xml_data = dicttoxml.dicttoxml(
43
47
  self.data,
44
48
  root=self.root,
@@ -0,0 +1,8 @@
1
+ """Shared JSON type aliases used by reader and converter APIs."""
2
+ from __future__ import annotations
3
+
4
+ from typing import TypeAlias
5
+
6
+ JSONValue: TypeAlias = None | bool | int | float | str | list["JSONValue"] | dict[str, "JSONValue"]
7
+
8
+ __all__ = ["JSONValue"]