• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alorence / django-modern-rpc / 18452590329

pending completion
18452590329

Pull #121

github

web-flow
Merge 7fa0c1b6f into a7cf0b823
Pull Request #121: Bump ruff from 0.13.3 to 0.14.0

166 of 166 branches covered (100.0%)

1383 of 1397 relevant lines covered (99.0%)

11.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.53
modernrpc/xmlrpc/backends/marshalling.py
1
from __future__ import annotations
13✔
2

3
import base64
13✔
4
from collections import OrderedDict
13✔
5
from datetime import datetime
13✔
6
from typing import Any, Callable, Generic, Iterable, Protocol, TypeVar
13✔
7

8
from modernrpc.exceptions import RPCInvalidRequest
13✔
9
from modernrpc.helpers import first
13✔
10
from modernrpc.types import DictStrAny, RpcErrorResult
13✔
11
from modernrpc.xmlrpc.backends.constants import MAXINT, MININT
13✔
12
from modernrpc.xmlrpc.handler import XmlRpcRequest, XmlRpcResult
13✔
13

14
# Self is available in typing base module only from Python 3.11
15
try:
13✔
16
    from typing import Self
13✔
17
except ImportError:
5✔
18
    from typing_extensions import Self
5✔
19

20
try:
13✔
21
    # types.NoneType is available only with Python 3.10+
22
    from types import NoneType
13✔
23
except ImportError:
1✔
24
    NoneType = type(None)  # type: ignore[misc]
1✔
25

26

27
class ElementTypeProtocol(Protocol, Iterable):
13✔
28
    """
29
    Base protocol for XML element types. This reflects the API of both the xml.etree and the lxml library.
30
    Unfortunately, since both libraries share the same interface without inheriting a common base class, we
31
    have to define our own.
32
    """
33

34
    tag: str
13✔
35
    text: str
13✔
36

37
    def find(self, path: str, namespaces=None) -> Self | None: ...
1✔
38
    def findall(self, path: str, namespaces=None) -> list[Self]: ...
1✔
39
    def append(self, sub_element: Self) -> None: ...
1✔
40

41

42
ElementType = TypeVar("ElementType", bound=ElementTypeProtocol)
13✔
43

44
LoadFuncType = Callable[[ElementType], Any]
13✔
45
DumpFuncType = Callable[[Any], ElementType]
13✔
46

47

48
class EtreeElementUnmarshaller(Generic[ElementType]):
13✔
49
    def __init__(self, allow_none=True) -> None:
13✔
50
        self.allow_none = allow_none
13✔
51

52
        self.load_funcs: dict[str, LoadFuncType] = {
13✔
53
            "value": self.load_value,
54
            "nil": self.load_nil,
55
            "boolean": self.load_bool,
56
            "int": self.load_int,
57
            "i4": self.load_int,
58
            "double": self.load_float,
59
            "string": self.load_str,
60
            "dateTime.iso8601": self.load_datetime,
61
            "base64": self.load_base64,
62
            "array": self.load_array,
63
            "struct": self.load_struct,
64
        }
65

66
    def element_to_request(self, root: ElementType) -> XmlRpcRequest:
13✔
67
        if root.tag != "methodCall":
13✔
68
            raise RPCInvalidRequest("missing methodCall tag", data=root)
13✔
69

70
        method_name = root.find("./methodName")
13✔
71
        if method_name is None:
13✔
72
            raise RPCInvalidRequest("missing methodCall.methodName tag", data=root)
13✔
73

74
        params = root.find("./params")
13✔
75
        if params is None:
13✔
76
            return XmlRpcRequest(method_name=self.stripped_text(method_name))
13✔
77

78
        param_list = params.findall("./param")
13✔
79

80
        args = [self.dispatch(self.first_child(param)) for param in param_list]
13✔
81
        return XmlRpcRequest(method_name=self.stripped_text(method_name), args=args)
13✔
82

83
    @staticmethod
13✔
84
    def stripped_text(elt: ElementType) -> str:
13✔
85
        return elt.text.strip() if elt.text else ""
13✔
86

87
    @staticmethod
13✔
88
    def first_child(elt: ElementType) -> ElementType:
13✔
89
        try:
13✔
90
            return first(elt)
13✔
91
        except IndexError as ie:
×
92
            raise RPCInvalidRequest("missing child element", data=elt) from ie
×
93

94
    def dispatch(self, elt: ElementType) -> Any:
13✔
95
        try:
13✔
96
            load_func = self.load_funcs[elt.tag]
13✔
97
        except KeyError as exc:
13✔
98
            raise RPCInvalidRequest(f"Unsupported type {elt.tag}") from exc
13✔
99

100
        return load_func(elt)
13✔
101

102
    def load_value(self, element: ElementType) -> Any:
13✔
103
        return self.dispatch(self.first_child(element))
13✔
104

105
    def load_nil(self, _: ElementType) -> None:
13✔
106
        if self.allow_none:
13✔
107
            return
13✔
108
        raise ValueError("cannot marshal None unless allow_none is enabled")
×
109

110
    def load_int(self, elt: ElementType) -> int:
13✔
111
        return int(self.stripped_text(elt))
13✔
112

113
    def load_bool(self, elt: ElementType) -> bool:
13✔
114
        value = self.stripped_text(elt)
13✔
115
        if value not in ("0", "1"):
13✔
116
            raise TypeError(f"Invalid boolean value: only 0 and 1 are allowed, found {value}")
13✔
117
        return value == "1"
13✔
118

119
    def load_float(self, elt: ElementType) -> float:
13✔
120
        return float(self.stripped_text(elt))
13✔
121

122
    def load_str(self, elt: ElementType) -> str:
13✔
123
        return str(self.stripped_text(elt))
13✔
124

125
    def load_datetime(self, elt: ElementType) -> datetime:
13✔
126
        return datetime.strptime(self.stripped_text(elt), "%Y%m%dT%H:%M:%S")
13✔
127

128
    def load_base64(self, elt: ElementType) -> bytes:
13✔
129
        return base64.b64decode(self.stripped_text(elt))
13✔
130

131
    def load_array(self, elt: ElementType) -> list[Any]:
13✔
132
        return [self.dispatch(value_elt) for value_elt in elt.findall("./data/value")]
13✔
133

134
    def load_struct(self, elt: ElementType) -> DictStrAny:
13✔
135
        member_names_and_values = [self.load_struct_member(member) for member in elt.findall("./member")]
13✔
136
        return dict(member_names_and_values)
13✔
137

138
    def load_struct_member(self, member_elt: ElementType) -> tuple[str, Any]:
13✔
139
        member_name = member_elt.find("./name")
13✔
140
        if member_name is None:
13✔
141
            raise RPCInvalidRequest("missing member.name tag", data=member_elt)
×
142
        value = member_elt.find("./value")
13✔
143
        if value is None:
13✔
144
            raise RPCInvalidRequest("missing member.value tag", data=member_elt)
×
145

146
        return self.stripped_text(member_name), self.dispatch(value)
13✔
147

148

149
class EtreeElementMarshaller(Generic[ElementType]):
13✔
150
    def __init__(
13✔
151
        self,
152
        element_factory: Callable[[str], ElementType],
153
        sub_element_factory: Callable[[ElementType, str], ElementType],
154
        allow_none=True,
155
    ) -> None:
156
        self.element_factory = element_factory
13✔
157
        self.sub_element_factory = sub_element_factory
13✔
158

159
        self.allow_none = allow_none
13✔
160

161
        self.dump_funcs: dict[type, DumpFuncType] = {
13✔
162
            NoneType: self.dump_nil,
163
            bool: self.dump_bool,
164
            int: self.dump_int,
165
            float: self.dump_float,
166
            str: self.dump_str,
167
            bytes: self.dump_bytearray,
168
            bytearray: self.dump_bytearray,
169
            datetime: self.dump_datetime,
170
            list: self.dump_list,
171
            tuple: self.dump_list,
172
            dict: self.dump_dict,
173
            OrderedDict: self.dump_dict,
174
        }
175

176
    def result_to_element(self, result: XmlRpcResult) -> ElementType:
13✔
177
        """Convert an XmlRpcResult to an XML element."""
178
        root = self.element_factory("methodResponse")
13✔
179

180
        if isinstance(result, RpcErrorResult):
13✔
181
            fault = self.sub_element_factory(root, "fault")
13✔
182
            value = self.sub_element_factory(fault, "value")
13✔
183

184
            struct = self.sub_element_factory(value, "struct")
13✔
185

186
            # Add faultCode member
187
            member = self.sub_element_factory(struct, "member")
13✔
188
            name = self.sub_element_factory(member, "name")
13✔
189
            name.text = "faultCode"
13✔
190
            value = self.sub_element_factory(member, "value")
13✔
191
            int_el = self.sub_element_factory(value, "int")
13✔
192
            int_el.text = str(result.code)
13✔
193

194
            # Add faultString member
195
            member = self.sub_element_factory(struct, "member")
13✔
196
            name = self.sub_element_factory(member, "name")
13✔
197
            name.text = "faultString"
13✔
198
            value = self.sub_element_factory(member, "value")
13✔
199
            string = self.sub_element_factory(value, "string")
13✔
200
            string.text = result.message
13✔
201
        else:
202
            params = self.sub_element_factory(root, "params")
13✔
203
            param = self.sub_element_factory(params, "param")
13✔
204
            value = self.sub_element_factory(param, "value")
13✔
205

206
            # Add the result data
207
            value.append(self.dispatch(result.data))
13✔
208

209
        return root
13✔
210

211
    def dispatch(self, value: Any) -> ElementType:
13✔
212
        """Dispatch a value to the appropriate dump method."""
213
        try:
13✔
214
            dump_func = self.dump_funcs[type(value)]
13✔
215
        except KeyError as exc:
13✔
216
            raise TypeError(f"Unsupported type: {type(value)}") from exc
13✔
217

218
        return dump_func(value)
13✔
219

220
    def dump_nil(self, _: None) -> ElementType:
13✔
221
        if self.allow_none:
13✔
222
            return self.element_factory("nil")
13✔
223
        raise ValueError("cannot marshal None unless allow_none is enabled")
×
224

225
    def dump_bool(self, value: bool) -> ElementType:
13✔
226
        boolean = self.element_factory("boolean")
13✔
227
        boolean.text = "1" if value else "0"
13✔
228
        return boolean
13✔
229

230
    def dump_int(self, value: int) -> ElementType:
13✔
231
        if value > MAXINT or value < MININT:
13✔
232
            raise OverflowError("int value exceeds XML-RPC limits")
13✔
233
        int_el = self.element_factory("int")
13✔
234
        int_el.text = str(value)
13✔
235
        return int_el
13✔
236

237
    def dump_float(self, value: float) -> ElementType:
13✔
238
        double = self.element_factory("double")
13✔
239
        double.text = str(value)
13✔
240
        return double
13✔
241

242
    def dump_str(self, value: str) -> ElementType:
13✔
243
        string = self.element_factory("string")
13✔
244
        string.text = value
13✔
245
        return string
13✔
246

247
    def dump_datetime(self, value: datetime) -> ElementType:
13✔
248
        dt = self.element_factory("dateTime.iso8601")
13✔
249
        dt.text = value.strftime("%04Y%02m%02dT%H:%M:%S")
13✔
250
        return dt
13✔
251

252
    def dump_bytearray(self, value: bytes | bytearray) -> ElementType:
13✔
253
        b64 = self.element_factory("base64")
13✔
254
        b64.text = base64.b64encode(value).decode()
13✔
255
        return b64
13✔
256

257
    def dump_dict(self, value: dict) -> ElementType:
13✔
258
        struct = self.element_factory("struct")
13✔
259
        for key, val in value.items():
13✔
260
            member = self.sub_element_factory(struct, "member")
13✔
261
            name = self.sub_element_factory(member, "name")
13✔
262
            name.text = str(key)
13✔
263
            value_el = self.sub_element_factory(member, "value")
13✔
264
            value_el.append(self.dispatch(val))
13✔
265
        return struct
13✔
266

267
    def dump_list(self, value: list | tuple) -> ElementType:
13✔
268
        array = self.element_factory("array")
13✔
269
        data = self.sub_element_factory(array, "data")
13✔
270
        for val in value:
13✔
271
            value_el = self.sub_element_factory(data, "value")
13✔
272
            value_el.append(self.dispatch(val))
13✔
273
        return array
13✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc