Completed
Push — master ( 36fcae...6a83ce )
by Ramon
01:12
created

jsons._common_impl.JsonSerializable.dumpb()   A

Complexity

Conditions 1

Size

Total Lines 8
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 2
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
"""
2
This module contains common implementation details of jsons. This module is
3
private, do not import (from) it directly.
4
"""
5
import json
6
import re
7
from typing import Dict
8
9
VALID_TYPES = (str, int, float, bool, list, tuple, set, dict, type(None))
10
RFC3339_DATETIME_PATTERN = '%Y-%m-%dT%H:%M:%S'
11
12
13
def dump(obj: object, cls: type = None, fork_inst: type = None,
14
         **kwargs) -> object:
15
    """
16
    Serialize the given ``obj`` to a JSON equivalent type (e.g. dict, list,
17
    int, ...).
18
19
    The way objects are serialized can be finetuned by setting serializer
20
    functions for the specific type using ``set_serializer``.
21
22
    You can also provide ``cls`` to specify that ``obj`` needs to be serialized
23
    as if it was of type ``cls`` (meaning to only take into account attributes
24
    from ``cls``). The type ``cls`` must have a ``__slots__`` defined. Any type
25
    will do, but in most cases you may want ``cls`` to be a base class of
26
    ``obj``.
27
    :param obj: a Python instance of any sort.
28
    :param cls: if given, ``obj`` will be dumped as if it is of type ``type``.
29
    :param fork_inst: if given, it uses this fork of ``JsonSerializable``.
30
    :param kwargs: the keyword args are passed on to the serializer function.
31
    :return: the serialized obj as a JSON type.
32
    """
33
    if cls and not hasattr(cls, '__slots__'):
34
        raise KeyError('Invalid type: "{}", only types that have a __slots__ '
35
                       'defined are allowed.'.format(cls.__name__))
36
    cls_ = cls or obj.__class__
37
    cls_name = cls_.__name__.lower()
38
    fork_inst = fork_inst or JsonSerializable
39
    serializer = fork_inst._serializers.get(cls_name, None)
40
    if not serializer:
41
        parents = [cls_ser for cls_ser in fork_inst._classes_serializers
42
                   if isinstance(obj, cls_ser)]
43
        if parents:
44
            pname = parents[0].__name__.lower()
45
            serializer = fork_inst._serializers[pname]
46
    kwargs_ = {'fork_inst': fork_inst, **kwargs}
47
    return serializer(obj, cls=cls, **kwargs_)
48
49
50
def load(json_obj: dict, cls: type = None, strict: bool = False,
51
         fork_inst: type = None, **kwargs) -> object:
52
    """
53
    Deserialize the given ``json_obj`` to an object of type ``cls``. If the
54
    contents of ``json_obj`` do not match the interface of ``cls``, a
55
    TypeError is raised.
56
57
    If ``json_obj`` contains a value that belongs to a custom class, there must
58
    be a type hint present for that value in ``cls`` to let this function know
59
    what type it should deserialize that value to.
60
61
62
    **Example**:
63
64
    >>> from typing import List
65
    >>> import jsons
66
    >>> class Person:
67
    ...     # No type hint required for name
68
    ...     def __init__(self, name):
69
    ...         self.name = name
70
    >>> class Family:
71
    ...     # Person is a custom class, use a type hint
72
    ...         def __init__(self, persons: List[Person]):
73
    ...             self.persons = persons
74
    >>> loaded = jsons.load({'persons': [{'name': 'John'}]}, Family)
75
    >>> loaded.persons[0].name
76
    'John'
77
78
    If no ``cls`` is given, a dict is simply returned, but contained values
79
    (e.g. serialized ``datetime`` values) are still deserialized.
80
81
    If `strict` mode is off and the type of `json_obj` exactly matches `cls`
82
    then `json_obj` is simply returned.
83
84
    :param json_obj: the dict that is to be deserialized.
85
    :param cls: a matching class of which an instance should be returned.
86
    :param strict: a bool to determine if a partially deserialized `json_obj`
87
    is tolerated.
88
    :param fork_inst: if given, it uses this fork of ``JsonSerializable``.
89
    :param kwargs: the keyword args are passed on to the deserializer function.
90
    :return: an instance of ``cls`` if given, a dict otherwise.
91
    """
92
    if not strict and type(json_obj) == cls:
0 ignored issues
show
introduced by
Using type() instead of isinstance() for a typecheck.
Loading history...
93
        return json_obj
94
    if type(json_obj) not in VALID_TYPES:
0 ignored issues
show
introduced by
Using type() instead of isinstance() for a typecheck.
Loading history...
95
        raise KeyError('Invalid type: "{}", only arguments of the following '
96
                       'types are allowed: {}'
97
                       .format(type(json_obj).__name__,
98
                               ", ".join(typ.__name__ for typ
99
                                         in VALID_TYPES)))
100
    cls = cls or type(json_obj)
101
    deserializer = _get_deserializer(cls, fork_inst)
102
    kwargs_ = {'strict': strict, 'fork_inst': fork_inst, **kwargs}
103
    return deserializer(json_obj, cls, **kwargs_)
104
105
106
def _get_deserializer(cls: type, fork_inst: type = None):
107
    fork_inst = fork_inst or JsonSerializable
108
    cls_name = cls.__name__ if hasattr(cls, '__name__') \
109
        else cls.__origin__.__name__
110
    deserializer = fork_inst._deserializers.get(cls_name.lower(), None)
111
    if not deserializer:
112
        parents = [cls_ for cls_ in fork_inst._classes_deserializers
113
                   if issubclass(cls, cls_)]
114
        if parents:
115
            pname = parents[0].__name__.lower()
116
            deserializer = fork_inst._deserializers[pname]
117
    return deserializer
118
119
120
class JsonSerializable:
0 ignored issues
show
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
121
    """
122
    This class offers an alternative to using the ``jsons.load`` and
123
    ``jsons.dump`` methods. An instance of a class that inherits from
124
    ``JsonSerializable`` has the ``json`` property, which value is equivalent
125
    to calling ``jsons.dump`` on that instance. Furthermore, you can call
126
    ``from_json`` on that class, which is equivalent to calling ``json.load``
127
    with that class as an argument.
128
    """
129
    _classes_serializers = list()
130
    _classes_deserializers = list()
131
    _serializers = dict()
132
    _deserializers = dict()
133
    _fork_counter = 0
134
135
    @classmethod
136
    def fork(cls, name: str = None) -> type:
137
        """
138
        Create a 'fork' of ``JsonSerializable``: a new ``type`` with a separate
139
        configuration of serializers and deserializers.
140
        :param name: the ``__name__`` of the new ``type``.
141
        :return: a new ``type`` based on ``JsonSerializable``.
142
        """
143
        cls._fork_counter += 1
144
        class_name = name or '{}_fork{}'.format(cls.__name__,
145
                                                cls._fork_counter)
146
        result = type(class_name, (cls,), {})
147
        result._classes_serializers = cls._classes_serializers.copy()
148
        result._classes_deserializers = cls._classes_deserializers.copy()
149
        result._serializers = cls._serializers.copy()
150
        result._deserializers = cls._deserializers.copy()
151
        result._fork_counter = 0
152
        return result
153
154
    @classmethod
155
    def with_dump(cls, fork: bool = False, **kwargs) -> type:
156
        """
157
        Return a class (``type``) that is based on JsonSerializable with the
158
        ``dump`` method being automatically provided the given ``kwargs``.
159
160
        **Example:**
161
162
        >>> custom_serializable = JsonSerializable\
163
                .with_dump(key_transformer=KEY_TRANSFORMER_CAMELCASE)
164
        >>> class Person(custom_serializable):
165
        ...     def __init__(self, my_name):
166
        ...         self.my_name = my_name
167
        >>> p = Person('John')
168
        >>> p.json
169
        {'myName': 'John'}
170
171
        :param kwargs: the keyword args that are automatically provided to the
172
        ``dump`` method.
173
        :param fork: determines that a new fork is to be created.
174
        :return: a class with customized behavior.
175
        """
176
        def _wrapper(inst, **kwargs_):
177
            return dump(inst, **{**kwargs_, **kwargs})
178
179
        type_ = cls.fork() if fork else cls
180
        type_.dump = _wrapper
181
        return type_
182
183
    @classmethod
184
    def with_load(cls, fork: bool = False, **kwargs) -> type:
185
        """
186
        Return a class (``type``) that is based on JsonSerializable with the
187
        ``load`` method being automatically provided the given ``kwargs``.
188
189
        **Example:**
190
191
        >>> custom_serializable = JsonSerializable\
192
                .with_load(key_transformer=KEY_TRANSFORMER_SNAKECASE)
193
        >>> class Person(custom_serializable):
194
        ...     def __init__(self, my_name):
195
        ...         self.my_name = my_name
196
        >>> p_json = {'myName': 'John'}
197
        >>> p = Person.from_json(p_json)
198
        >>> p.my_name
199
        'John'
200
201
        :param kwargs: the keyword args that are automatically provided to the
202
        ``load`` method.
203
        :param fork: determines that a new fork is to be created.
204
        :return: a class with customized behavior.
205
        """
206
        @classmethod
207
        def _wrapper(cls_, inst, **kwargs_):
208
            return load(inst, cls_, **{**kwargs_, **kwargs})
209
        type_ = cls.fork() if fork else cls
210
        type_.load = _wrapper
211
        return type_
212
213
    @property
214
    def json(self) -> object:
215
        """
216
        See ``jsons.dump``.
217
        :return: this instance in a JSON representation (dict).
218
        """
219
        return self.dump()
220
221
    def __str__(self) -> str:
222
        """
223
        See ``jsons.dumps``.
224
        :return: this instance as a JSON string.
225
        """
226
        return self.dumps()
227
228
    @classmethod
229
    def from_json(cls: type, json_obj: dict, **kwargs) -> object:
230
        """
231
        See ``jsons.load``.
232
        :param json_obj: a JSON representation of an instance of the inheriting
233
        class
234
        :param kwargs: the keyword args are passed on to the deserializer
235
        function.
236
        :return: an instance of the inheriting class.
237
        """
238
        return cls.load(json_obj, **kwargs)
239
240
    def dump(self, **kwargs) -> object:
241
        """
242
        See ``jsons.dump``.
243
        :param kwargs: the keyword args are passed on to the serializer
244
        function.
245
        :return: this instance in a JSON representation (dict).
246
        """
247
        return dump(self, fork_inst=self.__class__, **kwargs)
248
249
    @classmethod
250
    def load(cls: type, json_obj: dict, **kwargs) -> object:
251
        """
252
        See ``jsons.load``.
253
        :param kwargs: the keyword args are passed on to the serializer
254
        function.
255
        :param json_obj: the object that is loaded into an instance of `cls`.
256
        :return: this instance in a JSON representation (dict).
257
        """
258
        return load(json_obj, cls, fork_inst=cls, **kwargs)
259
260
    def dumps(self, **kwargs) -> str:
261
        """
262
        See ``jsons.dumps``.
263
        :param kwargs: the keyword args are passed on to the serializer
264
        function.
265
        :return: this instance as a JSON string.
266
        """
267
        return dumps(self, fork_inst=self.__class__, **kwargs)
268
269
    @classmethod
270
    def loads(cls: type, json_obj: str, **kwargs) -> object:
271
        """
272
        See ``jsons.loads``.
273
        :param kwargs: the keyword args are passed on to the serializer
274
        function.
275
        :param json_obj: the object that is loaded into an instance of `cls`.
276
        :return: this instance in a JSON representation (dict).
277
        """
278
        return loads(json_obj, cls, fork_inst=cls, **kwargs)
279
280
    def dumpb(self, **kwargs) -> bytes:
281
        """
282
        See ``jsons.dumpb``.
283
        :param kwargs: the keyword args are passed on to the serializer
284
        function.
285
        :return: this instance as a JSON string.
286
        """
287
        return dumpb(self, fork_inst=self.__class__, **kwargs)
288
289
    @classmethod
290
    def loadb(cls: type, json_obj: bytes, **kwargs) -> object:
291
        """
292
        See ``jsons.loadb``.
293
        :param kwargs: the keyword args are passed on to the serializer
294
        function.
295
        :param json_obj: the object that is loaded into an instance of `cls`.
296
        :return: this instance in a JSON representation (dict).
297
        """
298
        return loadb(json_obj, cls, fork_inst=cls, **kwargs)
299
300
    @classmethod
301
    def set_serializer(cls: type, func: callable, cls_: type,
302
                       high_prio: bool = True, fork: bool = False) -> type:
303
        """
304
        See ``jsons.set_serializer``.
305
        :param func: the serializer function.
306
        :param cls_: the type this serializer can handle.
307
        :param high_prio: determines the order in which is looked for the
308
        callable.
309
        :param fork: determines that a new fork is to be created.
310
        :return: the type on which this method is invoked or its fork.
311
        """
312
        type_ = cls.fork() if fork else cls
313
        set_serializer(func, cls_, high_prio, type_)
314
        return type_
315
316
    @classmethod
317
    def set_deserializer(cls: type, func: callable, cls_: type,
318
                         high_prio: bool = True, fork: bool = False) -> type:
319
        """
320
        See ``jsons.set_deserializer``.
321
        :param func: the deserializer function.
322
        :param cls_: the type this serializer can handle.
323
        :param high_prio: determines the order in which is looked for the
324
        callable.
325
        :param fork: determines that a new fork is to be created.
326
        :return: the type on which this method is invoked or its fork.
327
        """
328
        type_ = cls.fork() if fork else cls
329
        set_deserializer(func, cls_, high_prio, type_)
330
        return type_
331
332
333
def dumps(obj: object, jdkwargs: Dict[str, object] = None,
334
          *args, **kwargs) -> str:
335
    """
336
    Extend ``json.dumps``, allowing any Python instance to be dumped to a
337
    string. Any extra (keyword) arguments are passed on to ``json.dumps``.
338
339
    :param obj: the object that is to be dumped to a string.
340
    :param jdkwargs: extra keyword arguments for ``json.dumps`` (not
341
    ``jsons.dumps``!)
342
    :param args: extra arguments for ``jsons.dumps``.
343
    :param kwargs: Keyword arguments that are passed on through the
344
    serialization process.
345
    passed on to the serializer function.
346
    :return: ``obj`` as a ``str``.
347
    """
348
    jdkwargs = jdkwargs or {}
349
    dumped = dump(obj, *args, **kwargs)
350
    return json.dumps(dumped, **jdkwargs)
351
352
353
def loads(str_: str, cls: type = None, jdkwargs: Dict[str, object] = None,
354
          *args, **kwargs) -> object:
355
    """
356
    Extend ``json.loads``, allowing a string to be loaded into a dict or a
357
    Python instance of type ``cls``. Any extra (keyword) arguments are passed
358
    on to ``json.loads``.
359
360
    :param str_: the string that is to be loaded.
361
    :param cls: a matching class of which an instance should be returned.
362
    :param jdkwargs: extra keyword arguments for ``json.loads`` (not
363
    ``jsons.loads``!)
364
    :param args: extra arguments for ``jsons.loads``.
365
    :param kwargs: extra keyword arguments for ``jsons.loads``.
366
    :return: a JSON-type object (dict, str, list, etc.) or an instance of type
367
    ``cls`` if given.
368
    """
369
    jdkwargs = jdkwargs or {}
370
    obj = json.loads(str_, **jdkwargs)
371
    return load(obj, cls, *args, **kwargs)
372
373
374
def dumpb(obj: object, encoding: str = 'utf-8',
375
          jdkwargs: Dict[str, object] = None, *args, **kwargs) -> bytes:
376
    """
377
    Extend ``json.dumps``, allowing any Python instance to be dumped to bytes.
378
    Any extra (keyword) arguments are passed on to ``json.dumps``.
379
380
    :param obj: the object that is to be dumped to bytes.
381
    :param encoding: the encoding that is used to transform to bytes.
382
    :param jdkwargs: extra keyword arguments for ``json.dumps`` (not
383
    ``jsons.dumps``!)
384
    :param args: extra arguments for ``jsons.dumps``.
385
    :param kwargs: Keyword arguments that are passed on through the
386
    serialization process.
387
    passed on to the serializer function.
388
    :return: ``obj`` as ``bytes``.
389
    """
390
    jdkwargs = jdkwargs or {}
391
    dumped_dict = dump(obj, *args, **kwargs)
392
    dumped_str = json.dumps(dumped_dict, **jdkwargs)
393
    return dumped_str.encode(encoding=encoding)
394
395
396
def loadb(bytes_: bytes, cls: type = None, encoding: str = 'utf-8',
397
          jdkwargs: Dict[str, object] = None, *args, **kwargs) -> object:
398
    """
399
    Extend ``json.loads``, allowing bytes to be loaded into a dict or a Python
400
    instance of type ``cls``. Any extra (keyword) arguments are passed on to
401
    ``json.loads``.
402
403
    :param bytes_: the bytes that are to be loaded.
404
    :param cls: a matching class of which an instance should be returned.
405
    :param encoding: the encoding that is used to transform from bytes.
406
    :param jdkwargs: extra keyword arguments for ``json.loads`` (not
407
    ``jsons.loads``!)
408
    :param args: extra arguments for ``jsons.loads``.
409
    :param kwargs: extra keyword arguments for ``jsons.loads``.
410
    :return: a JSON-type object (dict, str, list, etc.) or an instance of type
411
    ``cls`` if given.
412
    """
413
    jdkwargs = jdkwargs or {}
414
    str_ = bytes_.decode(encoding=encoding)
415
    return loads(str_, cls, jdkwargs=jdkwargs, *args, **kwargs)
416
417
418
def set_serializer(func: callable, cls: type, high_prio: bool = True,
419
                   fork_inst: type = JsonSerializable) -> None:
420
    """
421
    Set a serializer function for the given type. You may override the default
422
    behavior of ``jsons.load`` by setting a custom serializer.
423
424
    The ``func`` argument must take one argument (i.e. the object that is to be
425
    serialized) and also a ``kwargs`` parameter. For example:
426
427
    >>> def func(obj, **kwargs):
428
    ...    return dict()
429
430
    You may ask additional arguments between ``cls`` and ``kwargs``.
431
432
    :param func: the serializer function.
433
    :param cls: the type this serializer can handle.
434
    :param high_prio: determines the order in which is looked for the callable.
435
    :param fork_inst: if given, it uses this fork of ``JsonSerializable``.
436
    :return: None.
437
    """
438
    if cls:
439
        index = 0 if high_prio else len(fork_inst._classes_serializers)
440
        fork_inst._classes_serializers.insert(index, cls)
441
        fork_inst._serializers[cls.__name__.lower()] = func
442
    else:
443
        fork_inst._serializers['nonetype'] = func
444
445
446
def set_deserializer(func: callable, cls: type, high_prio: bool = True,
447
                     fork_inst: type = JsonSerializable) -> None:
448
    """
449
    Set a deserializer function for the given type. You may override the
450
    default behavior of ``jsons.dump`` by setting a custom deserializer.
451
452
    The ``func`` argument must take two arguments (i.e. the dict containing the
453
    serialized values and the type that the values should be deserialized into)
454
    and also a ``kwargs`` parameter. For example:
455
456
    >>> def func(dict_, cls, **kwargs):
457
    ...    return cls()
458
459
    You may ask additional arguments between ``cls`` and ``kwargs``.
460
461
    :param func: the deserializer function.
462
    :param cls: the type this serializer can handle.
463
    :param high_prio: determines the order in which is looked for the callable.
464
    :param fork_inst: if given, it uses this fork of ``JsonSerializable``.
465
    :return: None.
466
    """
467
    if cls:
468
        index = 0 if high_prio else len(fork_inst._classes_deserializers)
469
        fork_inst._classes_deserializers.insert(index, cls)
470
        fork_inst._deserializers[cls.__name__.lower()] = func
471
    else:
472
        fork_inst._deserializers['nonetype'] = func
473
474
475
def camelcase(str_: str) -> str:
476
    """
477
    Return ``s`` in camelCase.
478
    :param str_: the string that is to be transformed.
479
    :return: a string in camelCase.
480
    """
481
    str_ = str_.replace('-', '_')
482
    splitted = str_.split('_')
483
    if len(splitted) > 1:
484
        str_ = ''.join([x.title() for x in splitted])
485
    return str_[0].lower() + str_[1:]
486
487
488
def snakecase(str_: str) -> str:
489
    """
490
    Return ``s`` in snake_case.
491
    :param str_: the string that is to be transformed.
492
    :return: a string in snake_case.
493
    """
494
    str_ = str_.replace('-', '_')
495
    str_ = str_[0].lower() + str_[1:]
496
    return re.sub(r'([a-z])([A-Z])', '\\1_\\2', str_).lower()
497
498
499
def pascalcase(str_: str) -> str:
500
    """
501
    Return ``s`` in PascalCase.
502
    :param str_: the string that is to be transformed.
503
    :return: a string in PascalCase.
504
    """
505
    camelcase_str = camelcase(str_)
506
    return camelcase_str[0].upper() + camelcase_str[1:]
507
508
509
def lispcase(str_: str) -> str:
510
    """
511
    Return ``s`` in lisp-case.
512
    :param str_: the string that is to be transformed.
513
    :return: a string in lisp-case.
514
    """
515
    return snakecase(str_).replace('_', '-')
516