json2xml 7.0.0__tar.gz → 7.1.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-7.0.0 → json2xml-7.1.0}/HISTORY.rst +21 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/PKG-INFO +2 -2
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml/__init__.py +1 -1
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml/backend_selector.py +7 -42
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml/cli.py +46 -49
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml/dicttoxml.py +63 -41
- json2xml-7.1.0/json2xml/dicttoxml_fast.py +278 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml/json2xml.py +11 -2
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml/utils.py +11 -3
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml.egg-info/PKG-INFO +2 -2
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml.egg-info/requires.txt +1 -1
- {json2xml-7.0.0 → json2xml-7.1.0}/pyproject.toml +2 -2
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/test_backend_selector.py +6 -16
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/test_cli.py +115 -1
- json2xml-7.1.0/tests/test_dicttoxml_fast_fallback.py +352 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/test_dicttoxml_unit.py +38 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/test_json2xml.py +48 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/test_rust_dicttoxml.py +33 -25
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/test_rust_python_parity.py +68 -3
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/test_utils.py +4 -3
- json2xml-7.0.0/json2xml/dicttoxml_fast.py +0 -248
- json2xml-7.0.0/tests/test_dicttoxml_fast_fallback.py +0 -207
- {json2xml-7.0.0 → json2xml-7.1.0}/AUTHORS.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/CONTRIBUTING.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/LICENSE +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/MANIFEST.in +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/README.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/Makefile +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/authors.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/benchmarks.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/conf.py +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/contributing.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/history.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/index.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/installation.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/json2xml.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/make.bat +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/modules.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/readme.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/rust_memory_benchmark.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/docs/usage.rst +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml/types.py +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml.egg-info/SOURCES.txt +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml.egg-info/dependency_links.txt +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml.egg-info/entry_points.txt +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/json2xml.egg-info/top_level.txt +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/setup.cfg +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/setup.py +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/__init__.py +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/conftest.py +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/test_benchmark_multi_python.py +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/test_benchmark_security_hardening.py +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/test_dict2xml.py +0 -0
- {json2xml-7.0.0 → json2xml-7.1.0}/tests/test_missing_coverage.py +0 -0
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
|
|
2
|
+
7.1.0 / 2026-09-03
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
* perf: enforce conversion depth and item limits inside the native payload walk, cutting a 2,000-record accelerated conversion from 7.1 ms to 1.4 ms
|
|
6
|
+
* fix: keep tuples on the Python serializer; the native backend rendered nested tuples differently under ``item_wrap=False`` and ``list_headers=True``
|
|
7
|
+
* fix: write identical CLI output to stdout and ``--output`` files, as UTF-8 bytes ending in exactly one newline
|
|
8
|
+
* fix: accept numbers and other scalars in ``dicttoxml_fast.escape_xml`` and ``wrap_cdata`` on both backends
|
|
9
|
+
* fix: raise ``InvalidDataError`` from ``Json2xml.to_xml`` for values the serializer rejects, such as a Mapping that is not a dict
|
|
10
|
+
* fix: classify ``datetime.time`` in the direct ``convert_kv`` helpers the same way the serializer does
|
|
11
|
+
* fix: report unreadable files distinctly from invalid JSON in ``readfromjson``
|
|
12
|
+
* refactor: load the optional Rust backend through one function that keeps working with ``json2xml-rs`` 0.5.0, and log under ``json2xml.dicttoxml_fast``
|
|
13
|
+
* refactor: share ``SerializerConfig`` between the serializer and the backend selector, add ``dicttoxml.serialize()``, and remove ``ConversionRequest`` and ``has_special_keys``
|
|
14
|
+
* refactor: declare CLI toggles with ``BooleanOptionalAction`` and derive type attributes from one exact-type table
|
|
15
|
+
* chore: release ``json2xml-rs`` 0.6.0 first and require it from ``json2xml[fast]``
|
|
16
|
+
|
|
17
|
+
7.0.1 / 2026-08-28
|
|
18
|
+
==================
|
|
19
|
+
|
|
20
|
+
* fix: validate explicitly provided empty ``--string``, ``--url``, and file-path inputs instead of silently falling back to stdin
|
|
21
|
+
|
|
22
|
+
|
|
2
23
|
7.0.0 / 2026-08-23
|
|
3
24
|
==================
|
|
4
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: json2xml
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.1.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
|
|
@@ -34,7 +34,7 @@ Requires-Dist: setuptools>=80.9.0; extra == "dev"
|
|
|
34
34
|
Requires-Dist: pygments>=2.20.0; extra == "dev"
|
|
35
35
|
Requires-Dist: xmltodict>=0.14.2; extra == "dev"
|
|
36
36
|
Provides-Extra: fast
|
|
37
|
-
Requires-Dist: json2xml-rs>=0.
|
|
37
|
+
Requires-Dist: json2xml-rs>=0.6.0; extra == "fast"
|
|
38
38
|
Dynamic: license-file
|
|
39
39
|
|
|
40
40
|
.. image:: ./json2xml-hero.png
|
|
@@ -1,30 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
4
3
|
from typing import Any, Protocol
|
|
5
4
|
|
|
6
5
|
# Shared with the Python serializer deliberately: the root-name gate below is only
|
|
7
6
|
# correct while it uses the exact predicate Python's name resolver starts from.
|
|
8
|
-
from .dicttoxml import _is_fast_valid_xml_name
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass(frozen=True, slots=True)
|
|
12
|
-
class ConversionRequest:
|
|
13
|
-
"""Normalized conversion request shared across backend adapters."""
|
|
14
|
-
|
|
15
|
-
obj: Any
|
|
16
|
-
root: bool
|
|
17
|
-
custom_root: str
|
|
18
|
-
ids: list[int] | None
|
|
19
|
-
attr_type: bool
|
|
20
|
-
item_wrap: bool
|
|
21
|
-
item_func: Any
|
|
22
|
-
cdata: bool
|
|
23
|
-
xml_namespaces: dict[str, Any] | None
|
|
24
|
-
list_headers: bool
|
|
25
|
-
xpath_format: bool
|
|
26
|
-
max_output_bytes: int | None = None
|
|
27
|
-
indent: str | None = None
|
|
7
|
+
from .dicttoxml import SerializerConfig, _is_fast_valid_xml_name
|
|
28
8
|
|
|
29
9
|
|
|
30
10
|
class BackendAdapter(Protocol):
|
|
@@ -34,10 +14,10 @@ class BackendAdapter(Protocol):
|
|
|
34
14
|
def name(self) -> str:
|
|
35
15
|
raise NotImplementedError # pragma: no cover
|
|
36
16
|
|
|
37
|
-
def can_handle(self, request:
|
|
17
|
+
def can_handle(self, request: SerializerConfig) -> bool:
|
|
38
18
|
raise NotImplementedError # pragma: no cover
|
|
39
19
|
|
|
40
|
-
def render(self, request:
|
|
20
|
+
def render(self, request: SerializerConfig) -> bytes:
|
|
41
21
|
raise NotImplementedError # pragma: no cover
|
|
42
22
|
|
|
43
23
|
|
|
@@ -47,7 +27,7 @@ class BackendSelector:
|
|
|
47
27
|
def __init__(self, *backends: BackendAdapter) -> None:
|
|
48
28
|
self._backends = backends
|
|
49
29
|
|
|
50
|
-
def render(self, request:
|
|
30
|
+
def render(self, request: SerializerConfig) -> bytes:
|
|
51
31
|
for backend in self._backends:
|
|
52
32
|
if backend.can_handle(request):
|
|
53
33
|
return backend.render(request)
|
|
@@ -56,9 +36,9 @@ class BackendSelector:
|
|
|
56
36
|
|
|
57
37
|
# Types the Rust backend renders byte-identically to the Python serializer. Subclasses are
|
|
58
38
|
# excluded on purpose: Python classifies them through its isinstance fallbacks, which the
|
|
59
|
-
# native writer does not reproduce.
|
|
39
|
+
# native writer does not reproduce. Tuples are excluded because Python applies its
|
|
40
|
+
# list-shape rules to them while the native writer only recognizes lists.
|
|
60
41
|
_RUST_SCALAR_TYPES = frozenset({str, bool, int, float, type(None)})
|
|
61
|
-
_RUST_CONTAINER_TYPES = frozenset({dict, list, tuple})
|
|
62
42
|
|
|
63
43
|
|
|
64
44
|
def _rust_renders_key_identically(key: Any) -> bool:
|
|
@@ -105,23 +85,8 @@ def rust_renders_identically(obj: Any) -> bool:
|
|
|
105
85
|
return False
|
|
106
86
|
stack.append(child)
|
|
107
87
|
continue
|
|
108
|
-
if value_type
|
|
88
|
+
if value_type is list:
|
|
109
89
|
stack.extend(value)
|
|
110
90
|
continue
|
|
111
91
|
return False
|
|
112
92
|
return True
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def has_special_keys(obj: Any) -> bool:
|
|
116
|
-
"""Return True when the payload uses Python-only special key semantics."""
|
|
117
|
-
if isinstance(obj, dict):
|
|
118
|
-
return any(
|
|
119
|
-
(isinstance(key, str) and (key.startswith("@") or key.endswith("@flat")))
|
|
120
|
-
or has_special_keys(value)
|
|
121
|
-
for key, value in obj.items()
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
if isinstance(obj, list):
|
|
125
|
-
return any(has_special_keys(item) for item in obj)
|
|
126
|
-
|
|
127
|
-
return False
|
|
@@ -7,10 +7,11 @@ Usage:
|
|
|
7
7
|
|
|
8
8
|
Flags:
|
|
9
9
|
-w, --wrapper string Wrapper element name (default "all")
|
|
10
|
-
-r, --root Include root element (default true)
|
|
11
|
-
-p, --pretty Pretty print output (default false)
|
|
12
|
-
-t, --type Include type attributes (default true)
|
|
13
|
-
-i, --item-wrap Wrap list items in <item> elements (default true
|
|
10
|
+
-r, --root Include root element (default true, --no-root disables)
|
|
11
|
+
-p, --pretty Pretty print output (default false, --no-pretty disables)
|
|
12
|
+
-t, --type Include type attributes (default true, --no-type disables)
|
|
13
|
+
-i, --item-wrap Wrap list items in <item> elements (default true,
|
|
14
|
+
--no-item-wrap disables)
|
|
14
15
|
-x, --xpath Use XPath 3.1 json-to-xml format
|
|
15
16
|
-o, --output string Output file (default: stdout)
|
|
16
17
|
-u, --url string Read JSON from URL
|
|
@@ -109,13 +110,14 @@ class CLIApplication:
|
|
|
109
110
|
"""Thin command adapter around input resolution, conversion, and output."""
|
|
110
111
|
|
|
111
112
|
def read_input(self, options: CLIConversionOptions) -> JSONValue:
|
|
112
|
-
|
|
113
|
+
# None means omitted; explicit empty values still require source validation.
|
|
114
|
+
if options.url is not None:
|
|
113
115
|
try:
|
|
114
116
|
return readfromurl(options.url)
|
|
115
117
|
except URLReadError as error:
|
|
116
118
|
exit_with_error(f"Error reading from URL: {error}")
|
|
117
119
|
|
|
118
|
-
if options.string:
|
|
120
|
+
if options.string is not None:
|
|
119
121
|
try:
|
|
120
122
|
return readfromstring(options.string)
|
|
121
123
|
except StringReadError as error:
|
|
@@ -125,9 +127,14 @@ class CLIApplication:
|
|
|
125
127
|
f"({error})"
|
|
126
128
|
)
|
|
127
129
|
|
|
128
|
-
if options.input_file:
|
|
130
|
+
if options.input_file is not None:
|
|
129
131
|
if options.input_file == "-":
|
|
130
132
|
return read_from_stdin()
|
|
133
|
+
if not options.input_file:
|
|
134
|
+
exit_with_error(
|
|
135
|
+
"Error: Empty JSON file path. "
|
|
136
|
+
"Pass a JSON file, use - for stdin, or provide --string/--url."
|
|
137
|
+
)
|
|
131
138
|
if not Path(options.input_file).is_file():
|
|
132
139
|
exit_with_error(
|
|
133
140
|
f"Error: JSON file not found: {options.input_file}. "
|
|
@@ -180,19 +187,32 @@ class CLIApplication:
|
|
|
180
187
|
return xml_output
|
|
181
188
|
|
|
182
189
|
def write_output(self, output: str | bytes, output_file: str | None) -> None:
|
|
183
|
-
if isinstance(output, bytes)
|
|
184
|
-
|
|
190
|
+
text = output.decode("utf-8") if isinstance(output, bytes) else output
|
|
191
|
+
# Both destinations get the same text: the document plus exactly one final
|
|
192
|
+
# newline, whether the serializer emitted none, one, or several.
|
|
193
|
+
text = text.rstrip("\n") + "\n"
|
|
185
194
|
|
|
186
|
-
if output_file:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
195
|
+
if not output_file:
|
|
196
|
+
self._write_stdout(text)
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
Path(output_file).write_text(text, encoding="utf-8")
|
|
201
|
+
except OSError as error:
|
|
202
|
+
exit_with_error(f"Error writing to file: {error}")
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def _write_stdout(text: str) -> None:
|
|
206
|
+
"""Write UTF-8 bytes so stdout matches the file output byte for byte."""
|
|
207
|
+
buffer = getattr(sys.stdout, "buffer", None)
|
|
208
|
+
if buffer is None:
|
|
209
|
+
# A text-only replacement such as io.StringIO has no byte layer.
|
|
210
|
+
sys.stdout.write(text)
|
|
193
211
|
return
|
|
194
212
|
|
|
195
|
-
|
|
213
|
+
sys.stdout.flush()
|
|
214
|
+
buffer.write(text.encode("utf-8"))
|
|
215
|
+
buffer.flush()
|
|
196
216
|
|
|
197
217
|
|
|
198
218
|
_APP = CLIApplication()
|
|
@@ -275,61 +295,38 @@ Examples:
|
|
|
275
295
|
default="all",
|
|
276
296
|
help='Wrapper element name (default: "all")',
|
|
277
297
|
)
|
|
298
|
+
# Each toggle also accepts its --no-* form; the last occurrence wins.
|
|
278
299
|
conv_group.add_argument(
|
|
279
300
|
"-r",
|
|
280
301
|
"--root",
|
|
281
302
|
dest="root",
|
|
282
|
-
action=
|
|
303
|
+
action=argparse.BooleanOptionalAction,
|
|
283
304
|
default=True,
|
|
284
|
-
help="Include root element
|
|
285
|
-
)
|
|
286
|
-
conv_group.add_argument(
|
|
287
|
-
"--no-root",
|
|
288
|
-
dest="root",
|
|
289
|
-
action="store_false",
|
|
290
|
-
help="Exclude root element",
|
|
305
|
+
help="Include root element",
|
|
291
306
|
)
|
|
292
307
|
conv_group.add_argument(
|
|
293
308
|
"-p",
|
|
294
309
|
"--pretty",
|
|
295
310
|
dest="pretty",
|
|
296
|
-
action=
|
|
311
|
+
action=argparse.BooleanOptionalAction,
|
|
297
312
|
default=False,
|
|
298
|
-
help="Pretty print output
|
|
299
|
-
)
|
|
300
|
-
conv_group.add_argument(
|
|
301
|
-
"--no-pretty",
|
|
302
|
-
dest="pretty",
|
|
303
|
-
action="store_false",
|
|
304
|
-
help="Disable pretty printing",
|
|
313
|
+
help="Pretty print output",
|
|
305
314
|
)
|
|
306
315
|
conv_group.add_argument(
|
|
307
316
|
"-t",
|
|
308
317
|
"--type",
|
|
309
318
|
dest="attr_type",
|
|
310
|
-
action=
|
|
319
|
+
action=argparse.BooleanOptionalAction,
|
|
311
320
|
default=True,
|
|
312
|
-
help="Include type attributes
|
|
313
|
-
)
|
|
314
|
-
conv_group.add_argument(
|
|
315
|
-
"--no-type",
|
|
316
|
-
dest="attr_type",
|
|
317
|
-
action="store_false",
|
|
318
|
-
help="Exclude type attributes",
|
|
321
|
+
help="Include type attributes",
|
|
319
322
|
)
|
|
320
323
|
conv_group.add_argument(
|
|
321
324
|
"-i",
|
|
322
325
|
"--item-wrap",
|
|
323
326
|
dest="item_wrap",
|
|
324
|
-
action=
|
|
327
|
+
action=argparse.BooleanOptionalAction,
|
|
325
328
|
default=True,
|
|
326
|
-
help="Wrap list items in <item> elements
|
|
327
|
-
)
|
|
328
|
-
conv_group.add_argument(
|
|
329
|
-
"--no-item-wrap",
|
|
330
|
-
dest="item_wrap",
|
|
331
|
-
action="store_false",
|
|
332
|
-
help="Don't wrap list items",
|
|
329
|
+
help="Wrap list items in <item> elements",
|
|
333
330
|
)
|
|
334
331
|
conv_group.add_argument(
|
|
335
332
|
"-x",
|
|
@@ -9,7 +9,7 @@ from fractions import Fraction
|
|
|
9
9
|
from functools import lru_cache
|
|
10
10
|
from io import BytesIO
|
|
11
11
|
from random import SystemRandom
|
|
12
|
-
from typing import Any, Union, cast
|
|
12
|
+
from typing import Any, TypeAlias, Union, cast
|
|
13
13
|
|
|
14
14
|
__lazy_modules__ = ["defusedxml.minidom"]
|
|
15
15
|
|
|
@@ -141,6 +141,10 @@ def get_unique_id(element: str) -> str:
|
|
|
141
141
|
return make_id(element)
|
|
142
142
|
|
|
143
143
|
|
|
144
|
+
# Only the truthiness of ``ids`` is ever consulted, so any value is a valid argument:
|
|
145
|
+
# the historical list form, a bool, or an int all enable generated id attributes.
|
|
146
|
+
IdsOption: TypeAlias = object
|
|
147
|
+
|
|
144
148
|
ELEMENT = Union[
|
|
145
149
|
str,
|
|
146
150
|
int,
|
|
@@ -214,6 +218,19 @@ def _classify(value: Any) -> int:
|
|
|
214
218
|
return _KIND_UNSUPPORTED
|
|
215
219
|
|
|
216
220
|
|
|
221
|
+
# Type attribute values for native JSON types, keyed by exact type like _EXACT_KINDS.
|
|
222
|
+
_EXACT_TYPE_NAMES: dict[type, str] = {
|
|
223
|
+
type(None): "null",
|
|
224
|
+
str: "str",
|
|
225
|
+
int: "int",
|
|
226
|
+
float: "float",
|
|
227
|
+
bool: "bool",
|
|
228
|
+
dict: "dict",
|
|
229
|
+
list: "list",
|
|
230
|
+
tuple: "list",
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
217
234
|
def get_xml_type(val: Any) -> str:
|
|
218
235
|
"""
|
|
219
236
|
Get the XML type of a given value.
|
|
@@ -224,21 +241,10 @@ def get_xml_type(val: Any) -> str:
|
|
|
224
241
|
Returns:
|
|
225
242
|
str: The XML type.
|
|
226
243
|
"""
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return "str"
|
|
232
|
-
if val_type is int:
|
|
233
|
-
return "int"
|
|
234
|
-
if val_type is float:
|
|
235
|
-
return "float"
|
|
236
|
-
if val_type is bool:
|
|
237
|
-
return "bool"
|
|
238
|
-
if val_type is dict:
|
|
239
|
-
return "dict"
|
|
240
|
-
if val_type is list or val_type is tuple:
|
|
241
|
-
return "list"
|
|
244
|
+
name = _EXACT_TYPE_NAMES.get(type(val))
|
|
245
|
+
if name is not None:
|
|
246
|
+
return name
|
|
247
|
+
# Subclasses and other Number implementations take the historical fallback names.
|
|
242
248
|
if isinstance(val, str):
|
|
243
249
|
return "str"
|
|
244
250
|
if _is_number(val):
|
|
@@ -514,7 +520,7 @@ def _append_xpath31(
|
|
|
514
520
|
|
|
515
521
|
def convert(
|
|
516
522
|
obj: Any,
|
|
517
|
-
ids:
|
|
523
|
+
ids: IdsOption,
|
|
518
524
|
attr_type: bool,
|
|
519
525
|
item_func: Callable[[str], str],
|
|
520
526
|
cdata: bool,
|
|
@@ -616,7 +622,7 @@ def list2xml_str(
|
|
|
616
622
|
|
|
617
623
|
def convert_dict(
|
|
618
624
|
obj: dict[str, Any],
|
|
619
|
-
ids:
|
|
625
|
+
ids: IdsOption,
|
|
620
626
|
parent: str,
|
|
621
627
|
attr_type: bool,
|
|
622
628
|
item_func: Callable[[str], str],
|
|
@@ -647,7 +653,7 @@ def convert_dict(
|
|
|
647
653
|
|
|
648
654
|
def convert_list(
|
|
649
655
|
items: Sequence[Any],
|
|
650
|
-
ids:
|
|
656
|
+
ids: IdsOption,
|
|
651
657
|
parent: str,
|
|
652
658
|
attr_type: bool,
|
|
653
659
|
item_func: Callable[[str], str],
|
|
@@ -679,7 +685,7 @@ def convert_list(
|
|
|
679
685
|
def _append_convert(
|
|
680
686
|
output: _XMLWriter,
|
|
681
687
|
obj: Any,
|
|
682
|
-
ids:
|
|
688
|
+
ids: IdsOption,
|
|
683
689
|
attr_type: bool,
|
|
684
690
|
item_func: Callable[[str], str],
|
|
685
691
|
cdata: bool,
|
|
@@ -755,7 +761,7 @@ def _append_dict2xml_str(
|
|
|
755
761
|
list_headers: bool = False,
|
|
756
762
|
) -> None:
|
|
757
763
|
"""Append a dict element using the same shape as dict2xml_str."""
|
|
758
|
-
ids:
|
|
764
|
+
ids: IdsOption = None
|
|
759
765
|
attr = dict(attr)
|
|
760
766
|
|
|
761
767
|
if attr_type:
|
|
@@ -821,7 +827,7 @@ def _append_dict2xml_str(
|
|
|
821
827
|
def _append_rawitem(
|
|
822
828
|
output: _XMLWriter,
|
|
823
829
|
rawitem: Any,
|
|
824
|
-
ids:
|
|
830
|
+
ids: IdsOption,
|
|
825
831
|
attr_type: bool,
|
|
826
832
|
item_func: Callable[[str], str],
|
|
827
833
|
cdata: bool,
|
|
@@ -863,7 +869,7 @@ def _append_list2xml_str(
|
|
|
863
869
|
item_wrap: bool,
|
|
864
870
|
list_headers: bool = False,
|
|
865
871
|
) -> None:
|
|
866
|
-
ids:
|
|
872
|
+
ids: IdsOption = None
|
|
867
873
|
attr = dict(attr)
|
|
868
874
|
if attr_type:
|
|
869
875
|
attr["type"] = get_xml_type(item)
|
|
@@ -908,7 +914,7 @@ def _append_list2xml_str(
|
|
|
908
914
|
def _append_convert_dict(
|
|
909
915
|
output: _XMLWriter,
|
|
910
916
|
obj: dict[str, Any],
|
|
911
|
-
ids:
|
|
917
|
+
ids: IdsOption,
|
|
912
918
|
parent: str,
|
|
913
919
|
attr_type: bool,
|
|
914
920
|
item_func: Callable[[str], str],
|
|
@@ -977,7 +983,7 @@ def _append_convert_dict(
|
|
|
977
983
|
def _append_convert_list(
|
|
978
984
|
output: _XMLWriter,
|
|
979
985
|
items: Sequence[Any],
|
|
980
|
-
ids:
|
|
986
|
+
ids: IdsOption,
|
|
981
987
|
parent: str,
|
|
982
988
|
attr_type: bool,
|
|
983
989
|
item_func: Callable[[str], str],
|
|
@@ -1067,9 +1073,18 @@ def _append_convert_list(
|
|
|
1067
1073
|
raise TypeError(f"Unsupported data type: {item} ({type(item).__name__})")
|
|
1068
1074
|
|
|
1069
1075
|
|
|
1076
|
+
_DATE_LIKE_TYPES = (datetime.datetime, datetime.date, datetime.time)
|
|
1077
|
+
|
|
1078
|
+
|
|
1070
1079
|
def convert_kv(
|
|
1071
1080
|
key: str,
|
|
1072
|
-
val: str
|
|
1081
|
+
val: str
|
|
1082
|
+
| int
|
|
1083
|
+
| float
|
|
1084
|
+
| numbers.Number
|
|
1085
|
+
| datetime.datetime
|
|
1086
|
+
| datetime.date
|
|
1087
|
+
| datetime.time,
|
|
1073
1088
|
attr_type: bool,
|
|
1074
1089
|
attr: dict[str, Any] | None = None,
|
|
1075
1090
|
cdata: bool = False,
|
|
@@ -1078,10 +1093,8 @@ def convert_kv(
|
|
|
1078
1093
|
attr = dict(attr) if attr else {}
|
|
1079
1094
|
key, attr = make_valid_xml_name(key, attr)
|
|
1080
1095
|
|
|
1081
|
-
#
|
|
1082
|
-
if
|
|
1083
|
-
val, (datetime.datetime, datetime.date)
|
|
1084
|
-
):
|
|
1096
|
+
# Date-like values serialize as ISO text, matching the main serializer path.
|
|
1097
|
+
if isinstance(val, _DATE_LIKE_TYPES):
|
|
1085
1098
|
val = val.isoformat()
|
|
1086
1099
|
|
|
1087
1100
|
if attr_type:
|
|
@@ -1094,15 +1107,19 @@ def convert_kv(
|
|
|
1094
1107
|
|
|
1095
1108
|
def convert_kv_valid_name(
|
|
1096
1109
|
key: str,
|
|
1097
|
-
val: str
|
|
1110
|
+
val: str
|
|
1111
|
+
| int
|
|
1112
|
+
| float
|
|
1113
|
+
| numbers.Number
|
|
1114
|
+
| datetime.datetime
|
|
1115
|
+
| datetime.date
|
|
1116
|
+
| datetime.time,
|
|
1098
1117
|
attr_type: bool,
|
|
1099
1118
|
attr: dict[str, Any],
|
|
1100
1119
|
cdata: bool = False,
|
|
1101
1120
|
) -> str:
|
|
1102
1121
|
"""Converts a scalar into an XML element when the caller already validated the key."""
|
|
1103
|
-
if
|
|
1104
|
-
val, (datetime.datetime, datetime.date)
|
|
1105
|
-
):
|
|
1122
|
+
if isinstance(val, _DATE_LIKE_TYPES):
|
|
1106
1123
|
val = val.isoformat()
|
|
1107
1124
|
|
|
1108
1125
|
attr_string = (
|
|
@@ -1168,12 +1185,12 @@ def convert_none_valid_name(key: str, attr_type: bool, attr: dict[str, Any]) ->
|
|
|
1168
1185
|
|
|
1169
1186
|
@dataclass(frozen=True, slots=True)
|
|
1170
1187
|
class SerializerConfig:
|
|
1171
|
-
"""Normalized options
|
|
1188
|
+
"""Normalized conversion options shared by the backend selector and this engine."""
|
|
1172
1189
|
|
|
1173
|
-
obj:
|
|
1190
|
+
obj: Any
|
|
1174
1191
|
root: bool
|
|
1175
1192
|
custom_root: str
|
|
1176
|
-
ids:
|
|
1193
|
+
ids: IdsOption
|
|
1177
1194
|
attr_type: bool
|
|
1178
1195
|
item_wrap: bool
|
|
1179
1196
|
item_func: Callable[[str], str]
|
|
@@ -1312,7 +1329,7 @@ def dicttoxml(
|
|
|
1312
1329
|
obj: ELEMENT,
|
|
1313
1330
|
root: bool = True,
|
|
1314
1331
|
custom_root: str = "root",
|
|
1315
|
-
ids:
|
|
1332
|
+
ids: IdsOption = None,
|
|
1316
1333
|
attr_type: bool = True,
|
|
1317
1334
|
item_wrap: bool = True,
|
|
1318
1335
|
item_func: Callable[[str], str] = default_item_func,
|
|
@@ -1337,9 +1354,9 @@ def dicttoxml(
|
|
|
1337
1354
|
Default is 'root'
|
|
1338
1355
|
allows you to specify a custom root element.
|
|
1339
1356
|
|
|
1340
|
-
:param
|
|
1341
|
-
Default is
|
|
1342
|
-
|
|
1357
|
+
:param ids:
|
|
1358
|
+
Default is None
|
|
1359
|
+
any truthy value gives elements unique ids.
|
|
1343
1360
|
|
|
1344
1361
|
:param bool attr_type:
|
|
1345
1362
|
Default is True
|
|
@@ -1484,4 +1501,9 @@ def dicttoxml(
|
|
|
1484
1501
|
max_output_bytes=max_output_bytes,
|
|
1485
1502
|
indent=indent,
|
|
1486
1503
|
)
|
|
1504
|
+
return serialize(config)
|
|
1505
|
+
|
|
1506
|
+
|
|
1507
|
+
def serialize(config: SerializerConfig) -> bytes:
|
|
1508
|
+
"""Render an already normalized configuration; see :func:`dicttoxml`."""
|
|
1487
1509
|
return _SerializerEngine(config).render()
|