Completed
Push — master ( 1c1494...590d9e )
by Ramon
01:54
created

jsons._common_impl.loadb()   A

Complexity

Conditions 1

Size

Total Lines 20
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nop 6
dl 0
loc 20
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) -> dict:
215
        """
216
        See ``jsons.dump``.
217
        :return: this instance in a JSON representation (dict).
218
        """
219
        return self.dump()
220
221
    @classmethod
222
    def from_json(cls: type, json_obj: dict, **kwargs) -> object:
223
        """
224
        See ``jsons.load``.
225
        :param json_obj: a JSON representation of an instance of the inheriting
226
        class
227
        :param kwargs: the keyword args are passed on to the deserializer
228
        function.
229
        :return: an instance of the inheriting class.
230
        """
231
        return cls.load(json_obj, **kwargs)
232
233
    def dump(self, **kwargs) -> dict:
234
        """
235
        See ``jsons.dump``.
236
        :param kwargs: the keyword args are passed on to the serializer
237
        function.
238
        :return: this instance in a JSON representation (dict).
239
        """
240
        return dump(self, fork_inst=self.__class__, **kwargs)
241
242
    @classmethod
243
    def load(cls: type, json_obj: dict, **kwargs) -> object:
244
        """
245
        See ``jsons.load``.
246
        :param kwargs: the keyword args are passed on to the serializer
247
        function.
248
        :return: this instance in a JSON representation (dict).
249
        """
250
        return load(json_obj, cls, fork_inst=cls, **kwargs)
251
252
    @classmethod
253
    def set_serializer(cls: type, func: callable, cls_: type,
254
                       high_prio: bool = True, fork: bool = False) -> type:
255
        """
256
        See ``jsons.set_serializer``.
257
        :param func: the serializer function.
258
        :param cls_: the type this serializer can handle.
259
        :param high_prio: determines the order in which is looked for the
260
        callable.
261
        :param fork: determines that a new fork is to be created.
262
        :return: the type on which this method is invoked or its fork.
263
        """
264
        type_ = cls.fork() if fork else cls
265
        set_serializer(func, cls_, high_prio, type_)
266
        return type_
267
268
    @classmethod
269
    def set_deserializer(cls: type, func: callable, cls_: type,
270
                         high_prio: bool = True, fork: bool = False) -> type:
271
        """
272
        See ``jsons.set_deserializer``.
273
        :param func: the deserializer function.
274
        :param cls_: the type this serializer can handle.
275
        :param high_prio: determines the order in which is looked for the
276
        callable.
277
        :param fork: determines that a new fork is to be created.
278
        :return: the type on which this method is invoked or its fork.
279
        """
280
        type_ = cls.fork() if fork else cls
281
        set_deserializer(func, cls_, high_prio, type_)
282
        return type_
283
284
285
def dumps(obj: object, jdkwargs: Dict[str, object] = None,
286
          *args, **kwargs) -> str:
287
    """
288
    Extend ``json.dumps``, allowing any Python instance to be dumped to a
289
    string. Any extra (keyword) arguments are passed on to ``json.dumps``.
290
291
    :param obj: the object that is to be dumped to a string.
292
    :param jdkwargs: extra keyword arguments for ``json.dumps`` (not
293
    ``jsons.dumps``!)
294
    :param args: extra arguments for ``jsons.dumps``.
295
    :param kwargs: Keyword arguments that are passed on through the
296
    serialization process.
297
    passed on to the serializer function.
298
    :return: ``obj`` as a ``str``.
299
    """
300
    jdkwargs = jdkwargs or {}
301
    return json.dumps(dump(obj, **jdkwargs), *args, **kwargs)
302
303
304
def loads(str_: str, cls: type = None, jdkwargs: Dict[str, object] = None,
305
          *args, **kwargs) -> object:
306
    """
307
    Extend ``json.loads``, allowing a string to be loaded into a dict or a
308
    Python instance of type ``cls``. Any extra (keyword) arguments are passed
309
    on to ``json.loads``.
310
311
    :param str_: the string that is to be loaded.
312
    :param cls: a matching class of which an instance should be returned.
313
    :param jdkwargs: extra keyword arguments for ``json.loads`` (not
314
    ``jsons.loads``!)
315
    :param args: extra arguments for ``jsons.loads``.
316
    :param kwargs: extra keyword arguments for ``jsons.loads``.
317
    :return: a JSON-type object (dict, str, list, etc.) or an instance of type
318
    ``cls`` if given.
319
    """
320
    jdkwargs = jdkwargs or {}
321
    obj = json.loads(str_, **jdkwargs)
322
    return load(obj, cls, *args, **kwargs)
323
324
325
def dumpb(obj: object, encoding: str = 'utf-8',
326
          jdkwargs: Dict[str, object] = None, *args, **kwargs) -> bytes:
327
    """
328
    Extend ``json.dumps``, allowing any Python instance to be dumped to bytes.
329
    Any extra (keyword) arguments are passed on to ``json.dumps``.
330
331
    :param obj: the object that is to be dumped to bytes.
332
    :param encoding: the encoding that is used to transform to bytes.
333
    :param jdkwargs: extra keyword arguments for ``json.dumps`` (not
334
    ``jsons.dumps``!)
335
    :param args: extra arguments for ``jsons.dumps``.
336
    :param kwargs: Keyword arguments that are passed on through the
337
    serialization process.
338
    passed on to the serializer function.
339
    :return: ``obj`` as ``bytes``.
340
    """
341
    jdkwargs = jdkwargs or {}
342
    dumped_dict = dump(obj, *args, **kwargs)
343
    dumped_str = json.dumps(dumped_dict, **jdkwargs)
344
    return dumped_str.encode(encoding=encoding)
345
346
347
def loadb(bytes_: bytes, cls: type = None, encoding: str = 'utf-8',
348
          jdkwargs: Dict[str, object] = None, *args, **kwargs) -> object:
349
    """
350
    Extend ``json.loads``, allowing bytes to be loaded into a dict or a Python
351
    instance of type ``cls``. Any extra (keyword) arguments are passed on to
352
    ``json.loads``.
353
354
    :param bytes_: the bytes that are to be loaded.
355
    :param cls: a matching class of which an instance should be returned.
356
    :param encoding: the encoding that is used to transform from bytes.
357
    :param jdkwargs: extra keyword arguments for ``json.loads`` (not
358
    ``jsons.loads``!)
359
    :param args: extra arguments for ``jsons.loads``.
360
    :param kwargs: extra keyword arguments for ``jsons.loads``.
361
    :return: a JSON-type object (dict, str, list, etc.) or an instance of type
362
    ``cls`` if given.
363
    """
364
    jdkwargs = jdkwargs or {}
365
    str_ = bytes_.decode(encoding=encoding)
366
    return loads(str_, cls, jdkwargs=jdkwargs, *args, **kwargs)
367
368
369
def set_serializer(func: callable, cls: type, high_prio: bool = True,
370
                   fork_inst: type = JsonSerializable) -> None:
371
    """
372
    Set a serializer function for the given type. You may override the default
373
    behavior of ``jsons.load`` by setting a custom serializer.
374
375
    The ``func`` argument must take one argument (i.e. the object that is to be
376
    serialized) and also a ``kwargs`` parameter. For example:
377
378
    >>> def func(obj, **kwargs):
379
    ...    return dict()
380
381
    You may ask additional arguments between ``cls`` and ``kwargs``.
382
383
    :param func: the serializer function.
384
    :param cls: the type this serializer can handle.
385
    :param high_prio: determines the order in which is looked for the callable.
386
    :param fork_inst: if given, it uses this fork of ``JsonSerializable``.
387
    :return: None.
388
    """
389
    if cls:
390
        index = 0 if high_prio else len(fork_inst._classes_serializers)
391
        fork_inst._classes_serializers.insert(index, cls)
392
        fork_inst._serializers[cls.__name__.lower()] = func
393
    else:
394
        fork_inst._serializers['nonetype'] = func
395
396
397
def set_deserializer(func: callable, cls: type, high_prio: bool = True,
398
                     fork_inst: type = JsonSerializable) -> None:
399
    """
400
    Set a deserializer function for the given type. You may override the
401
    default behavior of ``jsons.dump`` by setting a custom deserializer.
402
403
    The ``func`` argument must take two arguments (i.e. the dict containing the
404
    serialized values and the type that the values should be deserialized into)
405
    and also a ``kwargs`` parameter. For example:
406
407
    >>> def func(dict_, cls, **kwargs):
408
    ...    return cls()
409
410
    You may ask additional arguments between ``cls`` and ``kwargs``.
411
412
    :param func: the deserializer function.
413
    :param cls: the type this serializer can handle.
414
    :param high_prio: determines the order in which is looked for the callable.
415
    :param fork_inst: if given, it uses this fork of ``JsonSerializable``.
416
    :return: None.
417
    """
418
    if cls:
419
        index = 0 if high_prio else len(fork_inst._classes_deserializers)
420
        fork_inst._classes_deserializers.insert(index, cls)
421
        fork_inst._deserializers[cls.__name__.lower()] = func
422
    else:
423
        fork_inst._deserializers['nonetype'] = func
424
425
426
def camelcase(str_: str) -> str:
427
    """
428
    Return ``s`` in camelCase.
429
    :param str_: the string that is to be transformed.
430
    :return: a string in camelCase.
431
    """
432
    str_ = str_.replace('-', '_')
433
    splitted = str_.split('_')
434
    if len(splitted) > 1:
435
        str_ = ''.join([x.title() for x in splitted])
436
    return str_[0].lower() + str_[1:]
437
438
439
def snakecase(str_: str) -> str:
440
    """
441
    Return ``s`` in snake_case.
442
    :param str_: the string that is to be transformed.
443
    :return: a string in snake_case.
444
    """
445
    str_ = str_.replace('-', '_')
446
    str_ = str_[0].lower() + str_[1:]
447
    return re.sub(r'([a-z])([A-Z])', '\\1_\\2', str_).lower()
448
449
450
def pascalcase(str_: str) -> str:
451
    """
452
    Return ``s`` in PascalCase.
453
    :param str_: the string that is to be transformed.
454
    :return: a string in PascalCase.
455
    """
456
    camelcase_str = camelcase(str_)
457
    return camelcase_str[0].upper() + camelcase_str[1:]
458
459
460
def lispcase(str_: str) -> str:
461
    """
462
    Return ``s`` in lisp-case.
463
    :param str_: the string that is to be transformed.
464
    :return: a string in lisp-case.
465
    """
466
    return snakecase(str_).replace('_', '-')
467