-
Notifications
You must be signed in to change notification settings - Fork 0
/
_xmlify.py
152 lines (130 loc) · 5.37 KB
/
_xmlify.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
""" XMLable
A decorator to allow creation of xml config based on python dataclasses
Given a dataclass:
- Produce an xsd schema based on the class
- Produce an xml template based on the class
- Given any instance of the class, make a best-effort attempt at turning it into
a filled xml
- Create a parser for parsing the xml
"""
from humps import pascalize
from dataclasses import fields, is_dataclass
from typing import Any, dataclass_transform
from lxml.objectify import ObjectifiedElement
from lxml.etree import Element, _Element
from xmlable._utils import get, typename
from xmlable._errors import XError, XErrorCtx, ErrorTypes
from xmlable._manual import manual_xmlify
from xmlable._lxml_helpers import with_children, with_child, XMLSchema
from xmlable._xobject import XObject, gen_xobject
def validate_class(cls: type):
"""
Validate tha the class can be xmlified
- Must be a dataclass
- Cannot have any members called 'comment' (lxml parses comments as this tag)
- Cannot have
"""
if not is_dataclass(cls):
raise ErrorTypes.NotADataclass(cls)
reserved_attrs = ["get_xobject", "xsd_forward", "xsd_dependencies"]
# TODO: cleanup repetition
for f in fields(cls):
if f.name in reserved_attrs:
raise ErrorTypes.ReservedAttribute(cls, f.name)
elif f.name == "comment":
raise ErrorTypes.CommentAttribute(cls)
# JUSTIFY: Could potentially have added other attributes (of the class,
# rather than a field of an instance as provided by dataclass
# fields)
for reserved in reserved_attrs:
if hasattr(cls, reserved):
raise ErrorTypes.ReservedAttribute(cls, reserved)
if hasattr(cls, "comment"):
raise ErrorTypes.CommentAttribute(cls)
@dataclass_transform()
def xmlify(cls: type) -> type:
try:
validate_class(cls)
cls_name = typename(cls)
forward_decs = {cls}
meta_xobjects = [
(pascalize(f.name), f, gen_xobject(f.type, forward_decs))
for f in fields(cls)
]
class UserXObject(XObject):
def xsd_out(
self,
name: str,
attribs: dict[str, str] = {},
add_ns: dict[str, str] = {},
) -> _Element:
return Element(
f"{XMLSchema}element",
name=name,
type=cls_name,
attrib=attribs,
)
def xml_temp(self, name: str) -> _Element:
return with_children(
Element(name),
[
xobj.xml_temp(pascal_name)
for pascal_name, _, xobj in meta_xobjects
],
)
def xml_out(self, name: str, val: Any, ctx: XErrorCtx) -> _Element:
return with_children(
Element(name),
[
xobj.xml_out(
pascal_name,
get(val, m.name),
ctx.next(pascal_name),
)
for pascal_name, m, xobj in meta_xobjects
],
)
def xml_in(self, obj: ObjectifiedElement, ctx: XErrorCtx) -> Any:
parsed: dict[str, Any] = {}
for pascal_name, m, xobj in meta_xobjects:
if (m_obj := get(obj, pascal_name)) is not None:
parsed[m.name] = xobj.xml_in(
m_obj, ctx.next(pascal_name)
)
else:
raise ErrorTypes.NonMemberTag(ctx, cls, obj.tag, m.name)
return cls(**parsed)
cls_xobject = UserXObject()
# JUSTIFY: Why are xsd forward & dependencies not part of xobject?
# - xobject covers the use (not forward decs)
# - we want to present error messages to the user containing
# their types, so xsd dependencies are in terms of python
# types, rather than xobjects
# - forward and dependencies do not apply to the basic types,
# only user types
def xsd_forward(add_ns: dict[str, str]) -> _Element:
return with_child(
Element(f"{XMLSchema}complexType", name=cls_name),
with_children(
Element(f"{XMLSchema}sequence"),
[
xobj.xsd_out(pascal_name, attribs={}, add_ns=add_ns)
for pascal_name, m, xobj in meta_xobjects
],
),
)
def xsd_dependencies() -> set[type]:
return forward_decs
def get_xobject():
return cls_xobject
# helper methods for gen_xobject, and other dataclasses to generate their
# x methods
cls.xsd_forward = xsd_forward # type: ignore[attr-defined]
cls.xsd_dependencies = xsd_dependencies # type: ignore[attr-defined]
cls.get_xobject = get_xobject # type: ignore[attr-defined]
return manual_xmlify(cls)
except XError as e:
# NOTE: Trick to remove dirty 'internal' traceback, and raise from
# xmlify (makes more sense to user than seeing internals)
e.__traceback__ = None
raise e