File: serializers.py

package info (click to toggle)
pyro5 5.15-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,112 kB
  • sloc: python: 14,291; makefile: 163; sh: 66; javascript: 62
file content (494 lines) | stat: -rw-r--r-- 20,321 bytes parent folder | download
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
"""
The various serializers.

Pyro - Python Remote Objects.  Copyright by Irmen de Jong (irmen@razorvine.net).
"""

import array
import builtins
import uuid
import logging
import struct
import datetime
import decimal
import numbers
import inspect
import marshal
import json
import serpent
import contextlib
try:
    import msgpack
except ImportError:
    msgpack = None
from . import errors, config

__all__ = ["SerializerBase", "SerpentSerializer", "JsonSerializer", "MarshalSerializer", "MsgpackSerializer",
           "serializers", "serializers_by_id"]

log = logging.getLogger("Pyro5.serializers")


all_exceptions = {}
for name, t in vars(builtins).items():
    if type(t) is type and issubclass(t, BaseException):
        all_exceptions[name] = t
for name, t in vars(errors).items():
    if type(t) is type and issubclass(t, errors.PyroError):
        all_exceptions[name] = t


def pyro_class_serpent_serializer(obj, serializer, stream, level):
    # Override the default way that a Pyro URI/proxy/daemon is serialized.
    # Because it defines a __getstate__ it would otherwise just become a tuple,
    # and not be deserialized as a class.
    d = SerializerBase.class_to_dict(obj)
    serializer.ser_builtins_dict(d, stream, level)


def serialize_pyro_object_to_dict(obj):
    return {
        "__class__": "{:s}.{:s}".format(obj.__module__, obj.__class__.__name__),
        "state": obj.__getstate__()
    }


class SerializerBase(object):
    """Base class for (de)serializer implementations (which must be thread safe)"""
    serializer_id = 0  # define uniquely in subclass
    __custom_class_to_dict_registry = {}
    __custom_dict_to_class_registry = {}

    def loads(self, data):
        raise NotImplementedError("implement in subclass")

    def loadsCall(self, data):
        raise NotImplementedError("implement in subclass")

    def dumps(self, data):
        raise NotImplementedError("implement in subclass")

    def dumpsCall(self, obj, method, vargs, kwargs):
        raise NotImplementedError("implement in subclass")

    @classmethod
    def register_type_replacement(cls, object_type, replacement_function):
        raise NotImplementedError("implement in subclass")

    def _convertToBytes(self, data):
        if type(data) is bytearray:
            return bytes(data)
        if type(data) is memoryview:
            return data.tobytes()
        return data

    @classmethod
    def register_class_to_dict(cls, clazz, converter, serpent_too=True):
        """Registers a custom function that returns a dict representation of objects of the given class.
        The function is called with a single parameter; the object to be converted to a dict."""
        cls.__custom_class_to_dict_registry[clazz] = converter
        if serpent_too:
            with contextlib.suppress(errors.ProtocolError):
                def serpent_converter(obj, serializer, stream, level):
                    d = converter(obj)
                    serializer.ser_builtins_dict(d, stream, level)

                serpent.register_class(clazz, serpent_converter)

    @classmethod
    def unregister_class_to_dict(cls, clazz):
        """Removes the to-dict conversion function registered for the given class. Objects of the class
        will be serialized by the default mechanism again."""
        if clazz in cls.__custom_class_to_dict_registry:
            del cls.__custom_class_to_dict_registry[clazz]
        with contextlib.suppress(errors.ProtocolError):
            serpent.unregister_class(clazz)

    @classmethod
    def register_dict_to_class(cls, classname, converter):
        """
        Registers a custom converter function that creates objects from a dict with the given classname tag in it.
        The function is called with two parameters: the classname and the dictionary to convert to an instance of the class.
        """
        cls.__custom_dict_to_class_registry[classname] = converter

    @classmethod
    def unregister_dict_to_class(cls, classname):
        """
        Removes the converter registered for the given classname. Dicts with that classname tag
        will be deserialized by the default mechanism again.
        """
        if classname in cls.__custom_dict_to_class_registry:
            del cls.__custom_dict_to_class_registry[classname]

    @classmethod
    def class_to_dict(cls, obj):
        """
        Convert a non-serializable object to a dict. Partly borrowed from serpent.
        """
        for clazz in cls.__custom_class_to_dict_registry:
            if isinstance(obj, clazz):
                return cls.__custom_class_to_dict_registry[clazz](obj)
        if type(obj) in (set, dict, tuple, list):
            # we use a ValueError to mirror the exception type returned by serpent and other serializers
            raise ValueError("can't serialize type " + str(obj.__class__) + " into a dict")
        if hasattr(obj, "_pyroDaemon"):
            obj._pyroDaemon = None
        if isinstance(obj, BaseException):
            # special case for exceptions
            return {
                "__class__": obj.__class__.__module__ + "." + obj.__class__.__name__,
                "__exception__": True,
                "args": obj.args,
                "attributes": vars(obj)  # add custom exception attributes
            }
        # note: python 3.11+ object itself now has __getstate__
        has_own_getstate = (
            hasattr(type(obj), '__getstate__')
            and type(obj).__getstate__ is not getattr(object, '__getstate__', None)
        )
        if has_own_getstate:
            value = obj.__getstate__()
            if isinstance(value, dict):
                return value
        try:
            value = dict(vars(obj))  # make sure we can serialize anything that resembles a dict
            value["__class__"] = obj.__class__.__module__ + "." + obj.__class__.__name__
            return value
        except TypeError:
            if hasattr(obj, "__slots__"):
                # use the __slots__ instead of the vars dict
                value = {}
                for slot in obj.__slots__:
                    value[slot] = getattr(obj, slot)
                value["__class__"] = obj.__class__.__module__ + "." + obj.__class__.__name__
                return value
            else:
                raise errors.SerializeError("don't know how to serialize class " + str(obj.__class__) +
                                            " using serializer " + str(cls.__name__) +
                                            ". Give it vars() or an appropriate __getstate__")

    @classmethod
    def dict_to_class(cls, data):
        """
        Recreate an object out of a dict containing the class name and the attributes.
        Only a fixed set of classes are recognized.
        """
        from . import core, client, server  # circular imports...
        classname = data.get("__class__", "<unknown>")
        if isinstance(classname, bytes):
            classname = classname.decode("utf-8")
        if classname in cls.__custom_dict_to_class_registry:
            converter = cls.__custom_dict_to_class_registry[classname]
            return converter(classname, data)
        if "__" in classname:
            raise errors.SecurityError("refused to deserialize types with double underscores in their name: " + classname)
        # for performance reasons, the constructors below are hardcoded here
        # instead of added on a per-class basis to the dict-to-class registry
        if classname == "Pyro5.core.URI":
            uri = core.URI.__new__(core.URI)
            uri.__setstate__(data["state"])
            return uri
        elif classname == "Pyro5.client.Proxy":
            proxy = client.Proxy.__new__(client.Proxy)
            proxy.__setstate__(data["state"])
            return proxy
        elif classname == "Pyro5.server.Daemon":
            daemon = server.Daemon.__new__(server.Daemon)
            daemon.__setstate__(data["state"])
            return daemon
        elif classname.startswith("Pyro5.util."):
            if classname == "Pyro5.util.SerpentSerializer":
                return SerpentSerializer()
            elif classname == "Pyro5.util.MarshalSerializer":
                return MarshalSerializer()
            elif classname == "Pyro5.util.JsonSerializer":
                return JsonSerializer()
            elif classname == "Pyro5.util.MsgpackSerializer":
                return MsgpackSerializer()
        elif classname.startswith("Pyro5.errors."):
            errortype = getattr(errors, classname.split('.', 2)[2])
            if issubclass(errortype, errors.PyroError):
                return SerializerBase.make_exception(errortype, data)
        elif classname == "struct.error":
            return SerializerBase.make_exception(struct.error, data)
        elif classname == "Pyro5.core._ExceptionWrapper":
            ex = data["exception"]
            if isinstance(ex, dict) and "__class__" in ex:
                ex = SerializerBase.dict_to_class(ex)
            return core._ExceptionWrapper(ex)
        elif data.get("__exception__", False):
            if classname in all_exceptions:
                return SerializerBase.make_exception(all_exceptions[classname], data)
            # translate to the appropriate namespace...
            namespace, short_classname = classname.split('.', 1)
            if namespace in ("builtins", "exceptions"):
                exceptiontype = getattr(builtins, short_classname)
                if issubclass(exceptiontype, BaseException):
                    return SerializerBase.make_exception(exceptiontype, data)
            elif namespace == "sqlite3" and short_classname.endswith("Error"):
                import sqlite3
                exceptiontype = getattr(sqlite3, short_classname)
                if issubclass(exceptiontype, BaseException):
                    return SerializerBase.make_exception(exceptiontype, data)
        log.warning("unsupported serialized class: " + classname)
        raise errors.SerializeError("unsupported serialized class: " + classname)

    @staticmethod
    def make_exception(exceptiontype, data):
        ex = exceptiontype(*data["args"])
        if "attributes" in data:
            # restore custom attributes on the exception object
            for attr, value in data["attributes"].items():
                setattr(ex, attr, value)
        return ex

    def recreate_classes(self, literal):
        t = type(literal)
        if t is set:
            return {self.recreate_classes(x) for x in literal}
        if t is list:
            return [self.recreate_classes(x) for x in literal]
        if t is tuple:
            return tuple(self.recreate_classes(x) for x in literal)
        if t is dict:
            if "__class__" in literal:
                return self.dict_to_class(literal)
            result = {}
            for key, value in literal.items():
                result[key] = self.recreate_classes(value)
            return result
        return literal

    def __eq__(self, other):
        """this equality method is only to support the unit tests of this class"""
        return isinstance(other, SerializerBase) and vars(self) == vars(other)

    def __ne__(self, other):
        return not self.__eq__(other)

    __hash__ = object.__hash__


class SerpentSerializer(SerializerBase):
    """(de)serializer that wraps the serpent serialization protocol."""
    serializer_id = 1  # never change this

    def dumpsCall(self, obj, method, vargs, kwargs):
        return serpent.dumps((obj, method, vargs, kwargs), module_in_classname=True, bytes_repr=config.SERPENT_BYTES_REPR)

    def dumps(self, data):
        return serpent.dumps(data, module_in_classname=True, bytes_repr=config.SERPENT_BYTES_REPR)

    def loadsCall(self, data):
        obj, method, vargs, kwargs = serpent.loads(data)
        vargs = self.recreate_classes(vargs)
        kwargs = self.recreate_classes(kwargs)
        return obj, method, vargs, kwargs

    def loads(self, data):
        return self.recreate_classes(serpent.loads(data))

    @classmethod
    def register_type_replacement(cls, object_type, replacement_function):
        def custom_serializer(obj, serpent_serializer, outputstream, indentlevel):
            replaced = replacement_function(obj)
            if replaced is obj:
                serpent_serializer.ser_default_class(replaced, outputstream, indentlevel)
            else:
                serpent_serializer._serialize(replaced, outputstream, indentlevel)

        if object_type is type or not inspect.isclass(object_type):
            raise ValueError("refusing to register replacement for a non-type or the type 'type' itself")
        serpent.register_class(object_type, custom_serializer)

    @classmethod
    def dict_to_class(cls, data):
        if data.get("__class__") == "float":
            return float(data["value"])     # serpent encodes a float nan as a special class dict like this
        return super(SerpentSerializer, cls).dict_to_class(data)


class MarshalSerializer(SerializerBase):
    """(de)serializer that wraps the marshal serialization protocol."""
    serializer_id = 2  # never change this

    def dumpsCall(self, obj, method, vargs, kwargs):
        vargs = [self.convert_obj_into_marshallable(value) for value in vargs]
        kwargs = {key: self.convert_obj_into_marshallable(value) for key, value in kwargs.items()}
        return marshal.dumps((obj, method, vargs, kwargs))

    def dumps(self, data):
        return marshal.dumps(self.convert_obj_into_marshallable(data))

    def loadsCall(self, data):
        data = self._convertToBytes(data)
        obj, method, vargs, kwargs = marshal.loads(data)
        vargs = self.recreate_classes(vargs)
        kwargs = self.recreate_classes(kwargs)
        return obj, method, vargs, kwargs

    def loads(self, data):
        data = self._convertToBytes(data)
        return self.recreate_classes(marshal.loads(data))

    def convert_obj_into_marshallable(self, obj):
        marshalable_types = (str, int, float, type(None), bool, complex, bytes, bytearray,
                             tuple, set, frozenset, list, dict)
        if isinstance(obj, array.array):
            if obj.typecode == 'c':
                return obj.tostring()
            if obj.typecode == 'u':
                return obj.tounicode()
            return obj.tolist()
        if isinstance(obj, marshalable_types):
            return obj
        return self.class_to_dict(obj)

    @classmethod
    def class_to_dict(cls, obj):
        if isinstance(obj, uuid.UUID):
            return str(obj)
        return super(MarshalSerializer, cls).class_to_dict(obj)

    @classmethod
    def register_type_replacement(cls, object_type, replacement_function):
        pass  # marshal serializer doesn't support per-type hooks


class JsonSerializer(SerializerBase):
    """(de)serializer that wraps the json serialization protocol."""
    serializer_id = 3  # never change this

    __type_replacements = {}

    def dumpsCall(self, obj, method, vargs, kwargs):
        data = {"object": obj, "method": method, "params": vargs, "kwargs": kwargs}
        data = json.dumps(data, ensure_ascii=False, default=self.default)
        return data.encode("utf-8")

    def dumps(self, data):
        data = json.dumps(data, ensure_ascii=False, default=self.default)
        return data.encode("utf-8")

    def loadsCall(self, data):
        data = self._convertToBytes(data).decode("utf-8")
        data = json.loads(data)
        vargs = self.recreate_classes(data["params"])
        kwargs = self.recreate_classes(data["kwargs"])
        return data["object"], data["method"], vargs, kwargs

    def loads(self, data):
        data = self._convertToBytes(data).decode("utf-8")
        return self.recreate_classes(json.loads(data))

    def default(self, obj):
        replacer = self.__type_replacements.get(type(obj), None)
        if replacer:
            obj = replacer(obj)
        if isinstance(obj, set):
            return tuple(obj)  # json module can't deal with sets so we make a tuple out of it
        if isinstance(obj, uuid.UUID):
            return str(obj)
        if isinstance(obj, (datetime.datetime, datetime.date)):
            return obj.isoformat()
        if isinstance(obj, decimal.Decimal):
            return str(obj)
        if isinstance(obj, array.array):
            if obj.typecode == 'c':
                return obj.tostring()
            if obj.typecode == 'u':
                return obj.tounicode()
            return obj.tolist()
        return self.class_to_dict(obj)

    @classmethod
    def register_type_replacement(cls, object_type, replacement_function):
        if object_type is type or not inspect.isclass(object_type):
            raise ValueError("refusing to register replacement for a non-type or the type 'type' itself")
        cls.__type_replacements[object_type] = replacement_function


class MsgpackSerializer(SerializerBase):
    """(de)serializer that wraps the msgpack serialization protocol."""
    serializer_id = 4  # never change this

    __type_replacements = {}

    def dumpsCall(self, obj, method, vargs, kwargs):
        return msgpack.packb((obj, method, vargs, kwargs), use_bin_type=True, default=self.default)

    def dumps(self, data):
        return msgpack.packb(data, use_bin_type=True, default=self.default)

    def loadsCall(self, data):
        return msgpack.unpackb(self._convertToBytes(data), raw=False, object_hook=self.object_hook)

    def loads(self, data):
        return msgpack.unpackb(self._convertToBytes(data), raw=False, object_hook=self.object_hook, ext_hook=self.ext_hook)

    def default(self, obj):
        replacer = self.__type_replacements.get(type(obj), None)
        if replacer:
            obj = replacer(obj)
        if isinstance(obj, set):
            return tuple(obj)  # msgpack module can't deal with sets so we make a tuple out of it
        if isinstance(obj, uuid.UUID):
            return str(obj)
        if isinstance(obj, complex):
            return msgpack.ExtType(0x30, struct.pack("dd", obj.real, obj.imag))
        if isinstance(obj, datetime.datetime):
            if obj.tzinfo:
                raise errors.SerializeError("msgpack cannot serialize datetime with timezone info")
            return msgpack.ExtType(0x32, struct.pack("d", obj.timestamp()))
        if isinstance(obj, datetime.date):
            return msgpack.ExtType(0x33, struct.pack("l", obj.toordinal()))
        if isinstance(obj, decimal.Decimal):
            return str(obj)
        if isinstance(obj, numbers.Number):
            return msgpack.ExtType(0x31, str(obj).encode("ascii"))     # long
        if isinstance(obj, array.array):
            if obj.typecode == 'c':
                return obj.tostring()
            if obj.typecode == 'u':
                return obj.tounicode()
            return obj.tolist()
        return self.class_to_dict(obj)

    def object_hook(self, obj):
        if "__class__" in obj:
            return self.dict_to_class(obj)
        return obj

    def ext_hook(self, code, data):
        if code == 0x30:
            real, imag = struct.unpack("dd", data)
            return complex(real, imag)
        if code == 0x31:
            return int(data)
        if code == 0x32:
            return datetime.datetime.fromtimestamp(struct.unpack("d", data)[0])
        if code == 0x33:
            return datetime.date.fromordinal(struct.unpack("l", data)[0])
        raise errors.SerializeError("invalid ext code for msgpack: " + str(code))

    @classmethod
    def register_type_replacement(cls, object_type, replacement_function):
        if object_type is type or not inspect.isclass(object_type):
            raise ValueError("refusing to register replacement for a non-type or the type 'type' itself")
        cls.__type_replacements[object_type] = replacement_function


"""The various serializers that are supported"""
serializers = {
    "serpent": SerpentSerializer(),
    "marshal": MarshalSerializer(),
    "json": JsonSerializer()
}

if msgpack:
    serializers["msgpack"] = MsgpackSerializer()


"""The available serializers by their internal id"""
serializers_by_id = {ser.serializer_id: ser for ser in serializers.values()}