json2xml 6.4.0__tar.gz → 6.5.0__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.4.0 → json2xml-6.5.0}/HISTORY.rst +11 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/PKG-INFO +2 -2
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/__init__.py +1 -1
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/dicttoxml.py +153 -16
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml.egg-info/PKG-INFO +2 -2
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml.egg-info/requires.txt +1 -1
- {json2xml-6.4.0 → json2xml-6.5.0}/pyproject.toml +2 -2
- {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_dicttoxml_unit.py +135 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/AUTHORS.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/CONTRIBUTING.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/LICENSE +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/MANIFEST.in +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/README.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/Makefile +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/authors.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/benchmarks.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/conf.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/contributing.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/history.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/index.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/installation.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/json2xml.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/make.bat +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/modules.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/readme.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/rust_memory_benchmark.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/docs/usage.rst +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/backend_selector.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/cli.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/dicttoxml_fast.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/json2xml.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/types.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/utils.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml.egg-info/SOURCES.txt +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml.egg-info/dependency_links.txt +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml.egg-info/entry_points.txt +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/json2xml.egg-info/top_level.txt +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/requirements.in +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/setup.cfg +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/setup.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/tests/__init__.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/tests/conftest.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_backend_selector.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_benchmark_multi_python.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_cli.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_dict2xml.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_dicttoxml_fast_fallback.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_json2xml.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_missing_coverage.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_rust_dicttoxml.py +0 -0
- {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_utils.py +0 -0
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
|
|
2
|
+
6.5.0 / 2026-07-15
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
* perf: reduce pure Python serializer time by 31.1% on the deterministic 5,000-record benchmark through exact native-type dispatch
|
|
6
|
+
* perf: release ``json2xml-rs`` 0.4.2 with hybrid SIMD/word XML escape scanning and a 6.23% benchmark reduction
|
|
7
|
+
* fix: preserve numeric, string, dictionary, and sequence subclass behavior across the optimized Python dispatch paths
|
|
8
|
+
* test: require exactly 100% statement coverage from the complete Python suite and add dense Rust XML escape scanning regression coverage
|
|
9
|
+
* chore: require ``json2xml-rs>=0.4.2`` from ``json2xml[fast]`` and validate release wheels before PyPI publication
|
|
10
|
+
* docs: add Python and Rust before/after flamegraphs, buffer-capacity measurements, and release performance notes
|
|
11
|
+
|
|
12
|
+
|
|
2
13
|
6.4.0 / 2026-07-13
|
|
3
14
|
==================
|
|
4
15
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: json2xml
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.5.0
|
|
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
|
|
@@ -32,7 +32,7 @@ Requires-Dist: setuptools; extra == "dev"
|
|
|
32
32
|
Requires-Dist: pygments>=2.20.0; extra == "dev"
|
|
33
33
|
Requires-Dist: xmltodict>=0.12.0; extra == "dev"
|
|
34
34
|
Provides-Extra: fast
|
|
35
|
-
Requires-Dist: json2xml-rs>=0.4.
|
|
35
|
+
Requires-Dist: json2xml-rs>=0.4.2; extra == "fast"
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
38
|
.. image:: ./json2xml-hero.png
|
|
@@ -79,6 +79,16 @@ ELEMENT = Union[
|
|
|
79
79
|
]
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def _is_number(value: Any) -> bool:
|
|
83
|
+
"""Recognize common numbers without paying for abstract-class dispatch."""
|
|
84
|
+
value_type = type(value)
|
|
85
|
+
if value_type is bool:
|
|
86
|
+
return False
|
|
87
|
+
if value_type is int or value_type is float or value_type is complex:
|
|
88
|
+
return True
|
|
89
|
+
return isinstance(value, numbers.Number)
|
|
90
|
+
|
|
91
|
+
|
|
82
92
|
def get_xml_type(val: Any) -> str:
|
|
83
93
|
"""
|
|
84
94
|
Get the XML type of a given value.
|
|
@@ -100,7 +110,13 @@ def get_xml_type(val: Any) -> str:
|
|
|
100
110
|
return "float"
|
|
101
111
|
if val_type is bool:
|
|
102
112
|
return "bool"
|
|
103
|
-
if
|
|
113
|
+
if val_type is dict:
|
|
114
|
+
return "dict"
|
|
115
|
+
if val_type is list or val_type is tuple:
|
|
116
|
+
return "list"
|
|
117
|
+
if isinstance(val, str):
|
|
118
|
+
return "str"
|
|
119
|
+
if _is_number(val):
|
|
104
120
|
return "number"
|
|
105
121
|
if isinstance(val, dict):
|
|
106
122
|
return "dict"
|
|
@@ -120,7 +136,7 @@ def escape_xml(s: str | int | float | numbers.Number | None) -> str:
|
|
|
120
136
|
str: The escaped string.
|
|
121
137
|
"""
|
|
122
138
|
if isinstance(s, str):
|
|
123
|
-
if
|
|
139
|
+
if _XML_ESCAPE_CHARS.isdisjoint(s):
|
|
124
140
|
return s
|
|
125
141
|
s = s.replace("&", "&")
|
|
126
142
|
s = s.replace('"', """)
|
|
@@ -142,12 +158,13 @@ def make_attrstring(attr: dict[str, Any]) -> str:
|
|
|
142
158
|
"""
|
|
143
159
|
if not attr:
|
|
144
160
|
return ""
|
|
145
|
-
validate_xml_attr_names(attr)
|
|
146
161
|
if len(attr) == 1:
|
|
147
162
|
key, val = next(iter(attr.items()))
|
|
148
163
|
if key == "type":
|
|
149
164
|
return f' type="{val}"'
|
|
165
|
+
validate_xml_attr_names(attr)
|
|
150
166
|
return f' {key}="{escape_xml(val)}"'
|
|
167
|
+
validate_xml_attr_names(attr)
|
|
151
168
|
attrstring = " ".join(f'{k}="{escape_xml(v)}"' for k, v in attr.items())
|
|
152
169
|
return f" {attrstring}"
|
|
153
170
|
|
|
@@ -277,7 +294,7 @@ def get_xpath31_tag_name(val: Any) -> str:
|
|
|
277
294
|
return "boolean"
|
|
278
295
|
if isinstance(val, dict):
|
|
279
296
|
return "map"
|
|
280
|
-
if
|
|
297
|
+
if _is_number(val):
|
|
281
298
|
return "number"
|
|
282
299
|
if isinstance(val, str):
|
|
283
300
|
return "string"
|
|
@@ -368,7 +385,7 @@ def convert(
|
|
|
368
385
|
|
|
369
386
|
|
|
370
387
|
def is_primitive_type(val: Any) -> bool:
|
|
371
|
-
return val is None or isinstance(val, (str, bool
|
|
388
|
+
return val is None or isinstance(val, (str, bool)) or _is_number(val)
|
|
372
389
|
|
|
373
390
|
|
|
374
391
|
def dict2xml_str(
|
|
@@ -493,12 +510,45 @@ def _append_convert(
|
|
|
493
510
|
) -> None:
|
|
494
511
|
"""Append converted XML directly into output without building subtree strings."""
|
|
495
512
|
item_name = item_func(parent)
|
|
513
|
+
obj_type = type(obj)
|
|
496
514
|
|
|
497
|
-
|
|
515
|
+
# Exact built-ins stay ahead of ABC/subclass checks on this hot path. The
|
|
516
|
+
# later isinstance/_is_number branches intentionally preserve compatible
|
|
517
|
+
# str, numeric, dict, and sequence subclasses without charging native JSON
|
|
518
|
+
# values for abstract dispatch.
|
|
519
|
+
if obj_type is bool:
|
|
498
520
|
output.write(convert_bool(key=item_name, val=obj, attr_type=attr_type, cdata=cdata))
|
|
499
|
-
elif
|
|
521
|
+
elif obj_type is str:
|
|
500
522
|
output.write(convert_kv(key=item_name, val=obj, attr_type=attr_type, attr={}, cdata=cdata))
|
|
501
|
-
elif
|
|
523
|
+
elif obj_type is int or obj_type is float or obj_type is complex:
|
|
524
|
+
output.write(convert_kv(key=item_name, val=obj, attr_type=attr_type, attr={}, cdata=cdata))
|
|
525
|
+
elif obj is None:
|
|
526
|
+
output.write(convert_none(key=item_name, attr_type=attr_type, cdata=cdata))
|
|
527
|
+
elif obj_type is dict:
|
|
528
|
+
_append_convert_dict(
|
|
529
|
+
output,
|
|
530
|
+
cast("dict[str, Any]", obj),
|
|
531
|
+
ids,
|
|
532
|
+
parent,
|
|
533
|
+
attr_type,
|
|
534
|
+
item_func,
|
|
535
|
+
cdata,
|
|
536
|
+
item_wrap,
|
|
537
|
+
list_headers=list_headers,
|
|
538
|
+
)
|
|
539
|
+
elif obj_type is list or obj_type is tuple:
|
|
540
|
+
_append_convert_list(
|
|
541
|
+
output,
|
|
542
|
+
obj,
|
|
543
|
+
ids,
|
|
544
|
+
parent,
|
|
545
|
+
attr_type,
|
|
546
|
+
item_func,
|
|
547
|
+
cdata,
|
|
548
|
+
item_wrap,
|
|
549
|
+
list_headers=list_headers,
|
|
550
|
+
)
|
|
551
|
+
elif isinstance(obj, str) or _is_number(obj):
|
|
502
552
|
output.write(convert_kv(key=item_name, val=obj, attr_type=attr_type, attr={}, cdata=cdata))
|
|
503
553
|
elif hasattr(obj, "isoformat") and isinstance(obj, (datetime.datetime, datetime.date)):
|
|
504
554
|
output.write(
|
|
@@ -510,8 +560,6 @@ def _append_convert(
|
|
|
510
560
|
cdata=cdata,
|
|
511
561
|
)
|
|
512
562
|
)
|
|
513
|
-
elif obj is None:
|
|
514
|
-
output.write(convert_none(key=item_name, attr_type=attr_type, cdata=cdata))
|
|
515
563
|
elif isinstance(obj, dict):
|
|
516
564
|
_append_convert_dict(
|
|
517
565
|
output,
|
|
@@ -628,9 +676,24 @@ def _append_rawitem(
|
|
|
628
676
|
) -> None:
|
|
629
677
|
if rawitem is None:
|
|
630
678
|
return
|
|
631
|
-
|
|
679
|
+
rawitem_type = type(rawitem)
|
|
680
|
+
if rawitem_type is bool:
|
|
632
681
|
output.write("true" if rawitem else "false")
|
|
633
|
-
elif
|
|
682
|
+
elif rawitem_type is str or rawitem_type is int or rawitem_type is float or rawitem_type is complex:
|
|
683
|
+
output.write(escape_xml(str(rawitem)))
|
|
684
|
+
elif rawitem_type is dict or rawitem_type is list or rawitem_type is tuple:
|
|
685
|
+
_append_convert(
|
|
686
|
+
output,
|
|
687
|
+
rawitem,
|
|
688
|
+
ids,
|
|
689
|
+
attr_type,
|
|
690
|
+
item_func,
|
|
691
|
+
cdata,
|
|
692
|
+
item_wrap,
|
|
693
|
+
item_name,
|
|
694
|
+
list_headers=list_headers,
|
|
695
|
+
)
|
|
696
|
+
elif isinstance(rawitem, str) or _is_number(rawitem):
|
|
634
697
|
output.write(escape_xml(str(rawitem)))
|
|
635
698
|
else:
|
|
636
699
|
_append_convert(
|
|
@@ -708,15 +771,47 @@ def _append_convert_dict(
|
|
|
708
771
|
) -> None:
|
|
709
772
|
"""Append a dict as XML without allocating a joined child subtree."""
|
|
710
773
|
for key, val in obj.items():
|
|
774
|
+
val_type = type(val)
|
|
711
775
|
attr = {} if not ids else {"id": f"{get_unique_id(parent)}"}
|
|
712
776
|
key_is_flat = isinstance(key, str) and key.endswith("@flat")
|
|
713
777
|
xml_key = key[:-5] if key_is_flat else key
|
|
714
778
|
|
|
715
779
|
key, attr = make_valid_xml_name(xml_key, attr)
|
|
716
780
|
|
|
717
|
-
if
|
|
781
|
+
if val_type is bool:
|
|
718
782
|
output.write(convert_bool_valid_name(key, val, attr_type, attr))
|
|
719
|
-
elif
|
|
783
|
+
elif val_type is str or val_type is int or val_type is float or val_type is complex:
|
|
784
|
+
output.write(
|
|
785
|
+
convert_kv_valid_name(
|
|
786
|
+
key=key, val=val, attr_type=attr_type, attr=attr, cdata=cdata
|
|
787
|
+
)
|
|
788
|
+
)
|
|
789
|
+
elif val_type is dict:
|
|
790
|
+
_append_dict2xml_str(
|
|
791
|
+
output,
|
|
792
|
+
attr_type,
|
|
793
|
+
attr,
|
|
794
|
+
val,
|
|
795
|
+
item_func,
|
|
796
|
+
cdata,
|
|
797
|
+
key,
|
|
798
|
+
item_wrap,
|
|
799
|
+
False,
|
|
800
|
+
list_headers=list_headers,
|
|
801
|
+
)
|
|
802
|
+
elif val_type is list or val_type is tuple:
|
|
803
|
+
_append_list2xml_str(
|
|
804
|
+
output,
|
|
805
|
+
attr_type=attr_type,
|
|
806
|
+
attr=attr,
|
|
807
|
+
item=val,
|
|
808
|
+
item_func=item_func,
|
|
809
|
+
cdata=cdata,
|
|
810
|
+
item_name=f"{key}@flat" if key_is_flat else key,
|
|
811
|
+
item_wrap=item_wrap,
|
|
812
|
+
list_headers=list_headers,
|
|
813
|
+
)
|
|
814
|
+
elif isinstance(val, str) or _is_number(val):
|
|
720
815
|
output.write(
|
|
721
816
|
convert_kv_valid_name(
|
|
722
817
|
key=key, val=val, attr_type=attr_type, attr=attr, cdata=cdata
|
|
@@ -784,16 +879,58 @@ def _append_convert_list(
|
|
|
784
879
|
this_id = get_unique_id(parent) if ids else None
|
|
785
880
|
|
|
786
881
|
for i, item in enumerate(items):
|
|
882
|
+
item_type = type(item)
|
|
787
883
|
base_attr: dict[str, Any] | None = None
|
|
788
884
|
if ids:
|
|
789
885
|
base_attr = {"id": f"{this_id}_{i + 1}"}
|
|
790
886
|
|
|
791
|
-
if
|
|
887
|
+
if item_type is bool:
|
|
792
888
|
attr = dict(base_attr) if base_attr else {}
|
|
793
889
|
if item_name_attr:
|
|
794
890
|
attr.update(item_name_attr)
|
|
795
891
|
output.write(convert_bool_valid_name(item_name, item, attr_type, attr))
|
|
796
|
-
elif
|
|
892
|
+
elif item_type is str or item_type is int or item_type is float or item_type is complex:
|
|
893
|
+
attr = dict(base_attr) if base_attr else {}
|
|
894
|
+
if scalar_key_attr:
|
|
895
|
+
attr.update(scalar_key_attr)
|
|
896
|
+
output.write(
|
|
897
|
+
convert_kv_valid_name(
|
|
898
|
+
key=scalar_key,
|
|
899
|
+
val=item,
|
|
900
|
+
attr_type=attr_type,
|
|
901
|
+
attr=attr,
|
|
902
|
+
cdata=cdata,
|
|
903
|
+
)
|
|
904
|
+
)
|
|
905
|
+
elif item_type is dict:
|
|
906
|
+
attr = dict(base_attr) if base_attr else {}
|
|
907
|
+
_append_dict2xml_str(
|
|
908
|
+
output,
|
|
909
|
+
attr_type=attr_type,
|
|
910
|
+
attr=attr,
|
|
911
|
+
item=item,
|
|
912
|
+
item_func=item_func,
|
|
913
|
+
cdata=cdata,
|
|
914
|
+
item_name=item_name,
|
|
915
|
+
item_wrap=item_wrap,
|
|
916
|
+
parentIsList=True,
|
|
917
|
+
parent=parent,
|
|
918
|
+
list_headers=list_headers,
|
|
919
|
+
)
|
|
920
|
+
elif item_type is list or item_type is tuple:
|
|
921
|
+
attr = dict(base_attr) if base_attr else {}
|
|
922
|
+
_append_list2xml_str(
|
|
923
|
+
output,
|
|
924
|
+
attr_type=attr_type,
|
|
925
|
+
attr=attr,
|
|
926
|
+
item=item,
|
|
927
|
+
item_func=item_func,
|
|
928
|
+
cdata=cdata,
|
|
929
|
+
item_name=item_name,
|
|
930
|
+
item_wrap=item_wrap,
|
|
931
|
+
list_headers=list_headers,
|
|
932
|
+
)
|
|
933
|
+
elif isinstance(item, str) or _is_number(item):
|
|
797
934
|
attr = dict(base_attr) if base_attr else {}
|
|
798
935
|
if scalar_key_attr:
|
|
799
936
|
attr.update(scalar_key_attr)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: json2xml
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.5.0
|
|
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
|
|
@@ -32,7 +32,7 @@ Requires-Dist: setuptools; extra == "dev"
|
|
|
32
32
|
Requires-Dist: pygments>=2.20.0; extra == "dev"
|
|
33
33
|
Requires-Dist: xmltodict>=0.12.0; extra == "dev"
|
|
34
34
|
Provides-Extra: fast
|
|
35
|
-
Requires-Dist: json2xml-rs>=0.4.
|
|
35
|
+
Requires-Dist: json2xml-rs>=0.4.2; extra == "fast"
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
38
|
.. image:: ./json2xml-hero.png
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "json2xml"
|
|
7
|
-
version = "6.
|
|
7
|
+
version = "6.5.0"
|
|
8
8
|
description = "Simple Python Library to convert JSON to XML"
|
|
9
9
|
readme = "README.rst"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -50,7 +50,7 @@ dev = [
|
|
|
50
50
|
"pygments>=2.20.0",
|
|
51
51
|
"xmltodict>=0.12.0",
|
|
52
52
|
]
|
|
53
|
-
fast = ["json2xml-rs>=0.4.
|
|
53
|
+
fast = ["json2xml-rs>=0.4.2"]
|
|
54
54
|
|
|
55
55
|
[tool.pytest.ini_options]
|
|
56
56
|
testpaths = ["tests"]
|
|
@@ -24,6 +24,22 @@ class CustomNumber(numbers.Number):
|
|
|
24
24
|
return 7
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class StringSubclass(str):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class IntSubclass(int):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DictSubclass(dict[str, Any]):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ListSubclass(list[Any]):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
27
43
|
@pytest.mark.parametrize(
|
|
28
44
|
("value", "xml_type", "is_primitive"),
|
|
29
45
|
[
|
|
@@ -44,6 +60,125 @@ def test_get_xml_type_and_primitive_classification(value: Any, xml_type: str, is
|
|
|
44
60
|
assert dicttoxml.is_primitive_type(value) is is_primitive
|
|
45
61
|
|
|
46
62
|
|
|
63
|
+
@pytest.mark.parametrize(
|
|
64
|
+
("value", "expected"),
|
|
65
|
+
[
|
|
66
|
+
(1, True),
|
|
67
|
+
(3.5, True),
|
|
68
|
+
(1 + 2j, True),
|
|
69
|
+
(Decimal("1.25"), True),
|
|
70
|
+
(Fraction(3, 4), True),
|
|
71
|
+
(CustomNumber(), True),
|
|
72
|
+
(True, False),
|
|
73
|
+
("1", False),
|
|
74
|
+
],
|
|
75
|
+
)
|
|
76
|
+
# @lat: [[tests#XML helper behavior#Numeric fast path preserves general Number support]]
|
|
77
|
+
def test_number_classifier_preserves_supported_number_types(value: Any, expected: bool) -> None:
|
|
78
|
+
assert dicttoxml._is_number(value) is expected
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize(
|
|
82
|
+
("value", "expected"),
|
|
83
|
+
[
|
|
84
|
+
(StringSubclass("value"), "str"),
|
|
85
|
+
(DictSubclass(), "dict"),
|
|
86
|
+
(ListSubclass(), "list"),
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
def test_get_xml_type_preserves_container_subclasses(value: Any, expected: str) -> None:
|
|
90
|
+
assert dicttoxml.get_xml_type(value) == expected
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# @lat: [[tests#XML helper behavior#Exact-type dispatch preserves subclass fallbacks]]
|
|
94
|
+
def test_exact_type_dispatch_preserves_subclass_fallbacks() -> None:
|
|
95
|
+
data = DictSubclass({"values": ListSubclass([IntSubclass(7)])})
|
|
96
|
+
|
|
97
|
+
assert dicttoxml.dicttoxml(data) == (
|
|
98
|
+
b'<?xml version="1.0" encoding="UTF-8" ?>'
|
|
99
|
+
b'<root><values type="list"><item type="number">7</item></values></root>'
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_convert_preserves_root_scalar_and_sequence_subclasses() -> None:
|
|
104
|
+
def item_func(_parent: str) -> str:
|
|
105
|
+
return "item"
|
|
106
|
+
|
|
107
|
+
assert dicttoxml.convert(IntSubclass(7), [], True, item_func, False, True) == (
|
|
108
|
+
'<item type="number">7</item>'
|
|
109
|
+
)
|
|
110
|
+
assert dicttoxml.convert(ListSubclass([1]), [], True, item_func, False, True) == (
|
|
111
|
+
'<item type="int">1</item>'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_nested_subclasses_match_compatible_serializer_shapes() -> None:
|
|
116
|
+
def item_func(_parent: str) -> str:
|
|
117
|
+
return "item"
|
|
118
|
+
|
|
119
|
+
assert dicttoxml.convert_dict(
|
|
120
|
+
{"text": StringSubclass("value"), "mapping": DictSubclass({"count": 1})},
|
|
121
|
+
[],
|
|
122
|
+
"root",
|
|
123
|
+
True,
|
|
124
|
+
item_func,
|
|
125
|
+
False,
|
|
126
|
+
True,
|
|
127
|
+
) == (
|
|
128
|
+
'<text type="str">value</text>'
|
|
129
|
+
'<mapping type="dict"><count type="int">1</count></mapping>'
|
|
130
|
+
)
|
|
131
|
+
assert dicttoxml.convert_list(
|
|
132
|
+
[DictSubclass({"count": 1}), ListSubclass([2])],
|
|
133
|
+
[],
|
|
134
|
+
"items",
|
|
135
|
+
True,
|
|
136
|
+
item_func,
|
|
137
|
+
False,
|
|
138
|
+
True,
|
|
139
|
+
) == (
|
|
140
|
+
'<item type="dict"><count type="int">1</count></item>'
|
|
141
|
+
'<item type="list"><item type="int">2</item></item>'
|
|
142
|
+
)
|
|
143
|
+
assert dicttoxml.convert_list(
|
|
144
|
+
[IntSubclass(7)],
|
|
145
|
+
None,
|
|
146
|
+
"bad&parent",
|
|
147
|
+
True,
|
|
148
|
+
item_func,
|
|
149
|
+
False,
|
|
150
|
+
False,
|
|
151
|
+
) == '<key name="bad&parent" type="number">7</key>'
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_raw_attribute_values_preserve_mapping_subclasses() -> None:
|
|
155
|
+
assert dicttoxml.dict2xml_str(
|
|
156
|
+
attr_type=False,
|
|
157
|
+
attr={},
|
|
158
|
+
item={"@attrs": {"source": "api"}, "@val": DictSubclass({"count": 1})},
|
|
159
|
+
item_func=lambda _parent: "item",
|
|
160
|
+
cdata=False,
|
|
161
|
+
item_name="field",
|
|
162
|
+
item_wrap=True,
|
|
163
|
+
parentIsList=False,
|
|
164
|
+
) == '<field source="api"><count>1</count></field>'
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.mark.parametrize(
|
|
168
|
+
("value", "expected_text"),
|
|
169
|
+
[
|
|
170
|
+
(StringSubclass("A & B"), b"A & B"),
|
|
171
|
+
(IntSubclass(7), b"7"),
|
|
172
|
+
],
|
|
173
|
+
)
|
|
174
|
+
# @lat: [[tests#Conversion behavior#Raw attribute values preserve scalar subclasses]]
|
|
175
|
+
def test_raw_attribute_values_preserve_scalar_subclasses(value: str | int, expected_text: bytes) -> None:
|
|
176
|
+
assert dicttoxml.dicttoxml({"field": {"@attrs": {"source": "api"}, "@val": value}}) == (
|
|
177
|
+
b'<?xml version="1.0" encoding="UTF-8" ?>'
|
|
178
|
+
b'<root><field source="api">' + expected_text + b"</field></root>"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
47
182
|
@pytest.mark.parametrize(
|
|
48
183
|
"value",
|
|
49
184
|
[
|
|
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
|
|
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
|