msgspec-xml 0.1.1__tar.gz → 0.2.1__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.
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/PKG-INFO +1 -1
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/msgspec_xml/__init__.py +1 -1
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/msgspec_xml/decoder.py +17 -8
- msgspec_xml-0.2.1/msgspec_xml/encoder.py +112 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/msgspec_xml/introspection.py +47 -14
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/msgspec_xml/metadata.py +5 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/pylock.toml +114 -114
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/tests/resources/test-02.xml +1 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/tests/resources/test-03.xml +1 -1
- msgspec_xml-0.2.1/tests/resources/test-04.xml +8 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/tests/test_roundtrip.py +24 -4
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/uv.lock +116 -116
- msgspec_xml-0.1.1/msgspec_xml/encoder.py +0 -69
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/.gitattributes +0 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/.gitignore +0 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/.pre-commit-config.yaml +0 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/LICENSE +0 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/README.md +0 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/pdm.toml +0 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/pyproject.toml +0 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/renovate.json +0 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/tests/__init__.py +0 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/tests/resources/test-01.xml +0 -0
- {msgspec_xml-0.1.1 → msgspec_xml-0.2.1}/tests/test_metadata.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: msgspec-xml
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: XML Serialization/Deserialization using msgspec
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/msgspec-xml
|
|
6
6
|
Project-URL: Issues, https://codefloe.com/buriedincode/msgspec-xml/issues
|
|
@@ -10,9 +10,11 @@ from msgspec.structs import FieldInfo, fields
|
|
|
10
10
|
from msgspec_xml.introspection import (
|
|
11
11
|
FieldType,
|
|
12
12
|
attribute_name,
|
|
13
|
+
class_namespaces,
|
|
13
14
|
element_tag,
|
|
14
15
|
inspect_annotation,
|
|
15
16
|
item_tag,
|
|
17
|
+
wrapper_tag,
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
T = TypeVar("T", bound=Struct)
|
|
@@ -34,25 +36,29 @@ def decode(xml: str | bytes, type_: type[T]) -> T:
|
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
def _decode_struct(element: Element, cls: type[Struct]) -> dict[str, Any]:
|
|
39
|
+
namespaces = class_namespaces(cls=cls)
|
|
37
40
|
result: dict[str, Any] = {}
|
|
38
41
|
for field in fields(cls):
|
|
39
42
|
info = inspect_annotation(annotation=field.type)
|
|
40
|
-
value = _decode_field(element=element, field=field, info=info)
|
|
43
|
+
value = _decode_field(element=element, field=field, info=info, namespaces=namespaces)
|
|
41
44
|
if value is not _MISSING:
|
|
42
45
|
result[field.encode_name] = value
|
|
43
46
|
return result
|
|
44
47
|
|
|
45
48
|
|
|
46
|
-
def _decode_field(
|
|
49
|
+
def _decode_field(
|
|
50
|
+
element: Element, field: FieldInfo, info: FieldType, namespaces: dict[str, str]
|
|
51
|
+
) -> Any: # noqa: ANN401
|
|
47
52
|
xml = info.xml
|
|
48
53
|
if xml.attr:
|
|
49
|
-
name = attribute_name(field=field, info=info)
|
|
54
|
+
name = attribute_name(field=field, info=info, namespaces=namespaces)
|
|
50
55
|
return element.attrib.get(name, _MISSING)
|
|
51
56
|
if xml.text:
|
|
52
57
|
return (element.text or "").strip()
|
|
53
58
|
if info.item is not None:
|
|
54
|
-
return _decode_list(element=element, field=field, info=info)
|
|
55
|
-
|
|
59
|
+
return _decode_list(element=element, field=field, info=info, namespaces=namespaces)
|
|
60
|
+
tag = element_tag(field=field, info=info, namespaces=namespaces)
|
|
61
|
+
child = element.find(tag)
|
|
56
62
|
if child is None:
|
|
57
63
|
return _MISSING
|
|
58
64
|
if info.is_struct:
|
|
@@ -60,13 +66,16 @@ def _decode_field(element: Element, field: FieldInfo, info: FieldType) -> Any:
|
|
|
60
66
|
return (child.text or "").strip()
|
|
61
67
|
|
|
62
68
|
|
|
63
|
-
def _decode_list(
|
|
69
|
+
def _decode_list(
|
|
70
|
+
element: Element, field: FieldInfo, info: FieldType, namespaces: dict[str, str]
|
|
71
|
+
) -> list[Any]:
|
|
64
72
|
item = info.item
|
|
65
73
|
assert item is not None # noqa: S101
|
|
66
|
-
|
|
74
|
+
wrapper = wrapper_tag(info=info, namespaces=namespaces)
|
|
75
|
+
parent = element.find(wrapper) if wrapper else element
|
|
67
76
|
if parent is None:
|
|
68
77
|
return []
|
|
69
|
-
tag = item_tag(field=field, info=info)
|
|
78
|
+
tag = item_tag(field=field, info=info, namespaces=namespaces)
|
|
70
79
|
return [_decode_item(element=child, item=item) for child in parent.findall(tag)]
|
|
71
80
|
|
|
72
81
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
__all__ = ["Encoder", "encode"]
|
|
2
|
+
|
|
3
|
+
from xml.etree import ElementTree as ET
|
|
4
|
+
from xml.etree.ElementTree import Element
|
|
5
|
+
|
|
6
|
+
from msgspec import Struct, to_builtins
|
|
7
|
+
from msgspec.structs import FieldInfo, fields
|
|
8
|
+
|
|
9
|
+
from msgspec_xml.introspection import (
|
|
10
|
+
FieldType,
|
|
11
|
+
attribute_name,
|
|
12
|
+
class_namespaces,
|
|
13
|
+
class_tag,
|
|
14
|
+
element_tag,
|
|
15
|
+
inspect_annotation,
|
|
16
|
+
item_tag,
|
|
17
|
+
wrapper_tag,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Encoder:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
indent: int | None = None,
|
|
25
|
+
xml_declaration: bool = False,
|
|
26
|
+
short_empty_elements: bool = True,
|
|
27
|
+
) -> None:
|
|
28
|
+
if indent is not None and indent < 0:
|
|
29
|
+
raise ValueError("'indent' must be a non-negative integer")
|
|
30
|
+
self.indent = indent
|
|
31
|
+
self.xml_declaration = xml_declaration
|
|
32
|
+
self.short_empty_elements = short_empty_elements
|
|
33
|
+
|
|
34
|
+
def encode(self, obj: Struct) -> bytes:
|
|
35
|
+
root = _encode_struct(obj=obj, tag=class_tag(cls=type(obj)))
|
|
36
|
+
if self.indent is not None:
|
|
37
|
+
ET.indent(root, space=" " * self.indent)
|
|
38
|
+
return ET.tostring(
|
|
39
|
+
root,
|
|
40
|
+
encoding="UTF-8",
|
|
41
|
+
xml_declaration=self.xml_declaration,
|
|
42
|
+
short_empty_elements=self.short_empty_elements,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def encode(
|
|
47
|
+
obj: Struct,
|
|
48
|
+
indent: int | None = None,
|
|
49
|
+
xml_declaration: bool = False,
|
|
50
|
+
short_empty_elements: bool = True,
|
|
51
|
+
) -> bytes:
|
|
52
|
+
return Encoder(
|
|
53
|
+
indent=indent, xml_declaration=xml_declaration, short_empty_elements=short_empty_elements
|
|
54
|
+
).encode(obj=obj)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _encode_struct(obj: Struct, tag: str) -> Element:
|
|
58
|
+
cls = type(obj)
|
|
59
|
+
namespaces = class_namespaces(cls=cls)
|
|
60
|
+
for prefix, uri in namespaces.items():
|
|
61
|
+
ET.register_namespace(prefix, uri)
|
|
62
|
+
element = ET.Element(tag)
|
|
63
|
+
for field in fields(cls):
|
|
64
|
+
value = getattr(obj, field.name)
|
|
65
|
+
if value is None:
|
|
66
|
+
continue
|
|
67
|
+
info = inspect_annotation(annotation=field.type)
|
|
68
|
+
_encode_field(element=element, field=field, info=info, value=value, namespaces=namespaces)
|
|
69
|
+
return element
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _encode_field(
|
|
73
|
+
element: Element, field: FieldInfo, info: FieldType, value: object, namespaces: dict[str, str]
|
|
74
|
+
) -> None:
|
|
75
|
+
xml = info.xml
|
|
76
|
+
if xml.attr:
|
|
77
|
+
name = attribute_name(field=field, info=info, namespaces=namespaces)
|
|
78
|
+
element.set(name, _stringify(value=value))
|
|
79
|
+
elif xml.text:
|
|
80
|
+
element.text = _stringify(value)
|
|
81
|
+
elif info.item is not None:
|
|
82
|
+
_encode_list(element=element, field=field, info=info, value=value, namespaces=namespaces) # ty: ignore[invalid-argument-type]
|
|
83
|
+
elif info.is_struct:
|
|
84
|
+
struct_tag = element_tag(field=field, info=info, namespaces=namespaces)
|
|
85
|
+
element.append(_encode_struct(obj=value, tag=struct_tag)) # ty: ignore[invalid-argument-type]
|
|
86
|
+
else:
|
|
87
|
+
tag = element_tag(field=field, info=info, namespaces=namespaces)
|
|
88
|
+
child = ET.SubElement(element, tag)
|
|
89
|
+
child.text = _stringify(value)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _encode_list(
|
|
93
|
+
element: Element, field: FieldInfo, info: FieldType, value: list, namespaces: dict[str, str]
|
|
94
|
+
) -> None:
|
|
95
|
+
item = info.item
|
|
96
|
+
assert item is not None # noqa: S101
|
|
97
|
+
wrapper = wrapper_tag(info=info, namespaces=namespaces)
|
|
98
|
+
parent = ET.SubElement(element, wrapper) if wrapper else element
|
|
99
|
+
tag = item_tag(field=field, info=info, namespaces=namespaces)
|
|
100
|
+
for item_value in value:
|
|
101
|
+
if item.is_struct:
|
|
102
|
+
parent.append(_encode_struct(obj=item_value, tag=tag))
|
|
103
|
+
else:
|
|
104
|
+
child = ET.SubElement(parent, tag)
|
|
105
|
+
child.text = _stringify(item_value)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _stringify(value: object) -> str:
|
|
109
|
+
value = to_builtins(value)
|
|
110
|
+
if isinstance(value, bool):
|
|
111
|
+
return "true" if value else "false"
|
|
112
|
+
return str(value)
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
__all__ = [
|
|
2
2
|
"FieldType",
|
|
3
3
|
"attribute_name",
|
|
4
|
+
"class_namespaces",
|
|
4
5
|
"class_tag",
|
|
5
6
|
"element_tag",
|
|
6
7
|
"inspect_annotation",
|
|
7
8
|
"item_tag",
|
|
9
|
+
"qualify",
|
|
10
|
+
"wrapper_tag",
|
|
8
11
|
]
|
|
9
12
|
|
|
10
13
|
from types import NoneType
|
|
@@ -45,29 +48,59 @@ def class_tag(cls: type[Struct]) -> str:
|
|
|
45
48
|
return cls.__name__
|
|
46
49
|
|
|
47
50
|
|
|
48
|
-
def
|
|
49
|
-
return
|
|
51
|
+
def class_namespaces(cls: type[Struct]) -> dict[str, str]:
|
|
52
|
+
return getattr(cls, "__namespaces__", {})
|
|
50
53
|
|
|
51
54
|
|
|
52
|
-
def
|
|
55
|
+
def attribute_name(field: FieldInfo, info: FieldType, namespaces: dict[str, str]) -> str:
|
|
56
|
+
name = info.xml.tag or field.encode_name
|
|
57
|
+
return qualify(tag=name, ns=_resolve_ns(prefix=info.xml.ns, namespaces=namespaces))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def element_tag(field: FieldInfo, info: FieldType, namespaces: dict[str, str]) -> str:
|
|
53
61
|
if info.xml.tag:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
name = info.xml.tag
|
|
63
|
+
elif info.is_struct:
|
|
64
|
+
name = class_tag(cls=info.type)
|
|
65
|
+
else:
|
|
66
|
+
name = field.encode_name
|
|
67
|
+
return qualify(tag=name, ns=_resolve_ns(prefix=info.xml.ns, namespaces=namespaces))
|
|
58
68
|
|
|
59
69
|
|
|
60
|
-
def item_tag(field: FieldInfo, info: FieldType) -> str:
|
|
70
|
+
def item_tag(field: FieldInfo, info: FieldType, namespaces: dict[str, str]) -> str:
|
|
61
71
|
item = info.item
|
|
62
72
|
if item is None:
|
|
63
73
|
raise TypeError("item_tag() requires a list field")
|
|
74
|
+
prefix = item.xml.ns if item.xml.ns is not None else info.xml.ns
|
|
64
75
|
if item.xml.tag:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
name = item.xml.tag
|
|
77
|
+
elif info.xml.tag:
|
|
78
|
+
name = info.xml.tag
|
|
79
|
+
elif item.is_struct:
|
|
80
|
+
name = class_tag(cls=item.type)
|
|
81
|
+
else:
|
|
82
|
+
name = field.encode_name
|
|
83
|
+
return qualify(tag=name, ns=_resolve_ns(prefix=prefix, namespaces=namespaces))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def wrapper_tag(info: FieldType, namespaces: dict[str, str]) -> str | None:
|
|
87
|
+
if info.xml.wrapper is None:
|
|
88
|
+
return None
|
|
89
|
+
return qualify(tag=info.xml.wrapper, ns=_resolve_ns(prefix=info.xml.ns, namespaces=namespaces))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def qualify(tag: str, ns: str | None) -> str:
|
|
93
|
+
return f"{{{ns}}}{tag}" if ns else tag
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _resolve_ns(prefix: str | None, namespaces: dict[str, str]) -> str | None:
|
|
97
|
+
if prefix is None:
|
|
98
|
+
return None
|
|
99
|
+
if prefix not in namespaces:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Namespace prefix {prefix!r} is not declared in this Struct's __namespaces__."
|
|
102
|
+
)
|
|
103
|
+
return namespaces[prefix]
|
|
71
104
|
|
|
72
105
|
|
|
73
106
|
def _split_xml(annotation: Any) -> tuple[Any, XML]: # noqa: ANN401
|
|
@@ -8,6 +8,7 @@ class XML(Struct, frozen=True, forbid_unknown_fields=True):
|
|
|
8
8
|
wrapper: str | None = None
|
|
9
9
|
attr: bool = False
|
|
10
10
|
text: bool = False
|
|
11
|
+
ns: str | None = None
|
|
11
12
|
|
|
12
13
|
def __post_init__(self) -> None:
|
|
13
14
|
if self.attr and self.text:
|
|
@@ -16,10 +17,14 @@ class XML(Struct, frozen=True, forbid_unknown_fields=True):
|
|
|
16
17
|
raise ValueError("Wrapper elements cannot be used on XML attributes.")
|
|
17
18
|
if self.wrapper is not None and self.text:
|
|
18
19
|
raise ValueError("Wrapper elements cannot be used on XML text.")
|
|
20
|
+
if self.ns is not None and self.text:
|
|
21
|
+
raise ValueError("Namespaces cannot be used on XML text.")
|
|
19
22
|
if self.tag == "":
|
|
20
23
|
raise ValueError("tag cannot be empty.")
|
|
21
24
|
if self.wrapper == "":
|
|
22
25
|
raise ValueError("wrapper cannot be empty.")
|
|
26
|
+
if self.ns == "":
|
|
27
|
+
raise ValueError("ns cannot be empty.")
|
|
23
28
|
|
|
24
29
|
@property
|
|
25
30
|
def is_element(self) -> bool:
|