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.
Files changed (51) hide show
  1. {json2xml-6.4.0 → json2xml-6.5.0}/HISTORY.rst +11 -0
  2. {json2xml-6.4.0 → json2xml-6.5.0}/PKG-INFO +2 -2
  3. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/__init__.py +1 -1
  4. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/dicttoxml.py +153 -16
  5. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml.egg-info/PKG-INFO +2 -2
  6. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml.egg-info/requires.txt +1 -1
  7. {json2xml-6.4.0 → json2xml-6.5.0}/pyproject.toml +2 -2
  8. {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_dicttoxml_unit.py +135 -0
  9. {json2xml-6.4.0 → json2xml-6.5.0}/AUTHORS.rst +0 -0
  10. {json2xml-6.4.0 → json2xml-6.5.0}/CONTRIBUTING.rst +0 -0
  11. {json2xml-6.4.0 → json2xml-6.5.0}/LICENSE +0 -0
  12. {json2xml-6.4.0 → json2xml-6.5.0}/MANIFEST.in +0 -0
  13. {json2xml-6.4.0 → json2xml-6.5.0}/README.rst +0 -0
  14. {json2xml-6.4.0 → json2xml-6.5.0}/docs/Makefile +0 -0
  15. {json2xml-6.4.0 → json2xml-6.5.0}/docs/authors.rst +0 -0
  16. {json2xml-6.4.0 → json2xml-6.5.0}/docs/benchmarks.rst +0 -0
  17. {json2xml-6.4.0 → json2xml-6.5.0}/docs/conf.py +0 -0
  18. {json2xml-6.4.0 → json2xml-6.5.0}/docs/contributing.rst +0 -0
  19. {json2xml-6.4.0 → json2xml-6.5.0}/docs/history.rst +0 -0
  20. {json2xml-6.4.0 → json2xml-6.5.0}/docs/index.rst +0 -0
  21. {json2xml-6.4.0 → json2xml-6.5.0}/docs/installation.rst +0 -0
  22. {json2xml-6.4.0 → json2xml-6.5.0}/docs/json2xml.rst +0 -0
  23. {json2xml-6.4.0 → json2xml-6.5.0}/docs/make.bat +0 -0
  24. {json2xml-6.4.0 → json2xml-6.5.0}/docs/modules.rst +0 -0
  25. {json2xml-6.4.0 → json2xml-6.5.0}/docs/readme.rst +0 -0
  26. {json2xml-6.4.0 → json2xml-6.5.0}/docs/rust_memory_benchmark.rst +0 -0
  27. {json2xml-6.4.0 → json2xml-6.5.0}/docs/usage.rst +0 -0
  28. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/backend_selector.py +0 -0
  29. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/cli.py +0 -0
  30. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/dicttoxml_fast.py +0 -0
  31. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/json2xml.py +0 -0
  32. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/types.py +0 -0
  33. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml/utils.py +0 -0
  34. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml.egg-info/SOURCES.txt +0 -0
  35. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml.egg-info/dependency_links.txt +0 -0
  36. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml.egg-info/entry_points.txt +0 -0
  37. {json2xml-6.4.0 → json2xml-6.5.0}/json2xml.egg-info/top_level.txt +0 -0
  38. {json2xml-6.4.0 → json2xml-6.5.0}/requirements.in +0 -0
  39. {json2xml-6.4.0 → json2xml-6.5.0}/setup.cfg +0 -0
  40. {json2xml-6.4.0 → json2xml-6.5.0}/setup.py +0 -0
  41. {json2xml-6.4.0 → json2xml-6.5.0}/tests/__init__.py +0 -0
  42. {json2xml-6.4.0 → json2xml-6.5.0}/tests/conftest.py +0 -0
  43. {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_backend_selector.py +0 -0
  44. {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_benchmark_multi_python.py +0 -0
  45. {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_cli.py +0 -0
  46. {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_dict2xml.py +0 -0
  47. {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_dicttoxml_fast_fallback.py +0 -0
  48. {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_json2xml.py +0 -0
  49. {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_missing_coverage.py +0 -0
  50. {json2xml-6.4.0 → json2xml-6.5.0}/tests/test_rust_dicttoxml.py +0 -0
  51. {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.4.0
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.1; extra == "fast"
35
+ Requires-Dist: json2xml-rs>=0.4.2; extra == "fast"
36
36
  Dynamic: license-file
37
37
 
38
38
  .. image:: ./json2xml-hero.png
@@ -2,4 +2,4 @@
2
2
 
3
3
  __author__ = """Vinit Kumar"""
4
4
  __email__ = "mail@vinitkumar.me"
5
- __version__ = "6.4.0"
5
+ __version__ = "6.5.0"
@@ -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 isinstance(val, numbers.Number):
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 not _XML_ESCAPE_CHARS.intersection(s):
139
+ if _XML_ESCAPE_CHARS.isdisjoint(s):
124
140
  return s
125
141
  s = s.replace("&", "&amp;")
126
142
  s = s.replace('"', "&quot;")
@@ -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 isinstance(val, (int, float, numbers.Number)):
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, numbers.Number))
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
- if isinstance(obj, bool):
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 isinstance(obj, numbers.Number):
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 isinstance(obj, str):
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
- if isinstance(rawitem, bool):
679
+ rawitem_type = type(rawitem)
680
+ if rawitem_type is bool:
632
681
  output.write("true" if rawitem else "false")
633
- elif isinstance(rawitem, (str, numbers.Number)):
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 isinstance(val, bool):
781
+ if val_type is bool:
718
782
  output.write(convert_bool_valid_name(key, val, attr_type, attr))
719
- elif isinstance(val, (numbers.Number, str)):
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 isinstance(item, bool):
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 isinstance(item, (numbers.Number, str)):
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.4.0
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.1; extra == "fast"
35
+ Requires-Dist: json2xml-rs>=0.4.2; extra == "fast"
36
36
  Dynamic: license-file
37
37
 
38
38
  .. image:: ./json2xml-hero.png
@@ -10,4 +10,4 @@ pygments>=2.20.0
10
10
  xmltodict>=0.12.0
11
11
 
12
12
  [fast]
13
- json2xml-rs>=0.4.1
13
+ json2xml-rs>=0.4.2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "json2xml"
7
- version = "6.4.0"
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.1"]
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&amp;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 &amp; 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