Passed
Push — master ( 982cd4...cc2bc6 )
by Ramon
01:08
created

jsons._common_impl.load()   B

Complexity

Conditions 6

Size

Total Lines 56
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 15
nop 5
dl 0
loc 56
rs 8.6666
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 the deserializer should be strict (i.e. fail on a partially deserialized
87
    `json_obj` or on `None`).
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 (json_obj is None or type(json_obj) == cls):
93
        return json_obj
94
    if type(json_obj) not in VALID_TYPES:
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
    if json_obj is None:
101
        raise KeyError('Cannot load None with strict=True')
102
    cls = cls or type(json_obj)
103
    deserializer = _get_deserializer(cls, fork_inst)
104
    kwargs_ = {'strict': strict, 'fork_inst': fork_inst, **kwargs}
105
    return deserializer(json_obj, cls, **kwargs_)
106
107
108
def _get_deserializer(cls: type, fork_inst: type = None):
109
    fork_inst = fork_inst or JsonSerializable
110
    cls_name = cls.__name__ if hasattr(cls, '__name__') \
111
        else cls.__origin__.__name__
112
    deserializer = fork_inst._deserializers.get(cls_name.lower(), None)
113
    if not deserializer:
114
        parents = [cls_ for cls_ in fork_inst._classes_deserializers
115
                   if issubclass(cls, cls_)]
116
        if parents:
117
            pname = parents[0].__name__.lower()
118
            deserializer = fork_inst._deserializers[pname]
119
    return deserializer
120
121
122
class JsonSerializable:
123
    """
124
    This class offers an alternative to using the ``jsons.load`` and
125
    ``jsons.dump`` methods. An instance of a class that inherits from
126
    ``JsonSerializable`` has the ``json`` property, which value is equivalent
127
    to calling ``jsons.dump`` on that instance. Furthermore, you can call
128
    ``from_json`` on that class, which is equivalent to calling ``json.load``
129
    with that class as an argument.
130
    """
131
    _classes_serializers = list()
132
    _classes_deserializers = list()
133
    _serializers = dict()
134
    _deserializers = dict()
135
    _fork_counter = 0
136
137
    @classmethod
138
    def fork(cls, name: str = None) -> type:
139
        """
140
        Create a 'fork' of ``JsonSerializable``: a new ``type`` with a separate
141
        configuration of serializers and deserializers.
142
        :param name: the ``__name__`` of the new ``type``.
143
        :return: a new ``type`` based on ``JsonSerializable``.
144
        """
145
        cls._fork_counter += 1
146
        class_name = name or '{}_fork{}'.format(cls.__name__,
147
                                                cls._fork_counter)
148
        result = type(class_name, (cls,), {})
149
        result._classes_serializers = cls._classes_serializers.copy()
150
        result._classes_deserializers = cls._classes_deserializers.copy()
151
        result._serializers = cls._serializers.copy()
152
        result._deserializers = cls._deserializers.copy()
153
        result._fork_counter = 0
154
        return result
155
156
    @classmethod
157
    def with_dump(cls, fork: bool = False, **kwargs) -> type:
158
        """
159
        Return a class (``type``) that is based on JsonSerializable with the
160
        ``dump`` method being automatically provided the given ``kwargs``.
161
162
        **Example:**
163
164
        >>> custom_serializable = JsonSerializable\
165
                .with_dump(key_transformer=KEY_TRANSFORMER_CAMELCASE)
166
        >>> class Person(custom_serializable):
167
        ...     def __init__(self, my_name):
168
        ...         self.my_name = my_name
169
        >>> p = Person('John')
170
        >>> p.json
171
        {'myName': 'John'}
172
173
        :param kwargs: the keyword args that are automatically provided to the
174
        ``dump`` method.
175
        :param fork: determines that a new fork is to be created.
176
        :return: a class with customized behavior.
177
        """
178
        def _wrapper(inst, **kwargs_):
179
            return dump(inst, **{**kwargs_, **kwargs})
180
181
        type_ = cls.fork() if fork else cls
182
        type_.dump = _wrapper
183
        return type_
184
185
    @classmethod
186
    def with_load(cls, fork: bool = False, **kwargs) -> type:
187
        """
188
        Return a class (``type``) that is based on JsonSerializable with the
189
        ``load`` method being automatically provided the given ``kwargs``.
190
191
        **Example:**
192
193
        >>> custom_serializable = JsonSerializable\
194
                .with_load(key_transformer=KEY_TRANSFORMER_SNAKECASE)
195
        >>> class Person(custom_serializable):
196
        ...     def __init__(self, my_name):
197
        ...         self.my_name = my_name
198
        >>> p_json = {'myName': 'John'}
199
        >>> p = Person.from_json(p_json)
200
        >>> p.my_name
201
        'John'
202
203
        :param kwargs: the keyword args that are automatically provided to the
204
        ``load`` method.
205
        :param fork: determines that a new fork is to be created.
206
        :return: a class with customized behavior.
207
        """
208
        @classmethod
209
        def _wrapper(cls_, inst, **kwargs_):
210
            return load(inst, cls_, **{**kwargs_, **kwargs})
211
        type_ = cls.fork() if fork else cls
212
        type_.load = _wrapper
213
        return type_
214
215
    @property
216
    def json(self) -> object:
217
        """
218
        See ``jsons.dump``.
219
        :return: this instance in a JSON representation (dict).
220
        """
221
        return self.dump()
222
223
    def __str__(self) -> str:
224
        """
225
        See ``jsons.dumps``.
226
        :return: this instance as a JSON string.
227
        """
228
        return self.dumps()
229
230
    @classmethod
231
    def from_json(cls: type, json_obj: dict, **kwargs) -> object:
232
        """
233
        See ``jsons.load``.
234
        :param json_obj: a JSON representation of an instance of the inheriting
235
        class
236
        :param kwargs: the keyword args are passed on to the deserializer
237
        function.
238
        :return: an instance of the inheriting class.
239
        """
240
        return cls.load(json_obj, **kwargs)
241
242
    def dump(self, **kwargs) -> object:
243
        """
244
        See ``jsons.dump``.
245
        :param kwargs: the keyword args are passed on to the serializer
246
        function.
247
        :return: this instance in a JSON representation (dict).
248
        """
249
        return dump(self, fork_inst=self.__class__, **kwargs)
250
251
    @classmethod
252
    def load(cls: type, json_obj: dict, **kwargs) -> object:
253
        """
254
        See ``jsons.load``.
255
        :param kwargs: the keyword args are passed on to the serializer
256
        function.
257
        :param json_obj: the object that is loaded into an instance of `cls`.
258
        :return: this instance in a JSON representation (dict).
259
        """
260
        return load(json_obj, cls, fork_inst=cls, **kwargs)
261
262
    def dumps(self, **kwargs) -> str:
263
        """
264
        See ``jsons.dumps``.
265
        :param kwargs: the keyword args are passed on to the serializer
266
        function.
267
        :return: this instance as a JSON string.
268
        """
269
        return dumps(self, fork_inst=self.__class__, **kwargs)
270
271
    @classmethod
272
    def loads(cls: type, json_obj: str, **kwargs) -> object:
273
        """
274
        See ``jsons.loads``.
275
        :param kwargs: the keyword args are passed on to the serializer
276
        function.
277
        :param json_obj: the object that is loaded into an instance of `cls`.
278
        :return: this instance in a JSON representation (dict).
279
        """
280
        return loads(json_obj, cls, fork_inst=cls, **kwargs)
281
282
    def dumpb(self, **kwargs) -> bytes:
283
        """
284
        See ``jsons.dumpb``.
285
        :param kwargs: the keyword args are passed on to the serializer
286
        function.
287
        :return: this instance as a JSON string.
288
        """
289
        return dumpb(self, fork_inst=self.__class__, **kwargs)
290
291
    @classmethod
292
    def loadb(cls: type, json_obj: bytes, **kwargs) -> object:
293
        """
294
        See ``jsons.loadb``.
295
        :param kwargs: the keyword args are passed on to the serializer
296
        function.
297
        :param json_obj: the object that is loaded into an instance of `cls`.
298
        :return: this instance in a JSON representation (dict).
299
        """
300
        return loadb(json_obj, cls, fork_inst=cls, **kwargs)
301
302
    @classmethod
303
    def set_serializer(cls: type, func: callable, cls_: type,
304
                       high_prio: bool = True, fork: bool = False) -> type:
305
        """
306
        See ``jsons.set_serializer``.
307
        :param func: the serializer function.
308
        :param cls_: the type this serializer can handle.
309
        :param high_prio: determines the order in which is looked for the
310
        callable.
311
        :param fork: determines that a new fork is to be created.
312
        :return: the type on which this method is invoked or its fork.
313
        """
314
        type_ = cls.fork() if fork else cls
315
        set_serializer(func, cls_, high_prio, type_)
316
        return type_
317
318
    @classmethod
319
    def set_deserializer(cls: type, func: callable, cls_: type,
320
                         high_prio: bool = True, fork: bool = False) -> type:
321
        """
322
        See ``jsons.set_deserializer``.
323
        :param func: the deserializer function.
324
        :param cls_: the type this serializer can handle.
325
        :param high_prio: determines the order in which is looked for the
326
        callable.
327
        :param fork: determines that a new fork is to be created.
328
        :return: the type on which this method is invoked or its fork.
329
        """
330
        type_ = cls.fork() if fork else cls
331
        set_deserializer(func, cls_, high_prio, type_)
332
        return type_
333
334
335
def dumps(obj: object, jdkwargs: Dict[str, object] = None,
336
          *args, **kwargs) -> str:
337
    """
338
    Extend ``json.dumps``, allowing any Python instance to be dumped to a
339
    string. Any extra (keyword) arguments are passed on to ``json.dumps``.
340
341
    :param obj: the object that is to be dumped to a string.
342
    :param jdkwargs: extra keyword arguments for ``json.dumps`` (not
343
    ``jsons.dumps``!)
344
    :param args: extra arguments for ``jsons.dumps``.
345
    :param kwargs: Keyword arguments that are passed on through the
346
    serialization process.
347
    passed on to the serializer function.
348
    :return: ``obj`` as a ``str``.
349
    """
350
    jdkwargs = jdkwargs or {}
351
    dumped = dump(obj, *args, **kwargs)
352
    return json.dumps(dumped, **jdkwargs)
353
354
355
def loads(str_: str, cls: type = None, jdkwargs: Dict[str, object] = None,
356
          *args, **kwargs) -> object:
357
    """
358
    Extend ``json.loads``, allowing a string to be loaded into a dict or a
359
    Python instance of type ``cls``. Any extra (keyword) arguments are passed
360
    on to ``json.loads``.
361
362
    :param str_: the string that is to be loaded.
363
    :param cls: a matching class of which an instance should be returned.
364
    :param jdkwargs: extra keyword arguments for ``json.loads`` (not
365
    ``jsons.loads``!)
366
    :param args: extra arguments for ``jsons.loads``.
367
    :param kwargs: extra keyword arguments for ``jsons.loads``.
368
    :return: a JSON-type object (dict, str, list, etc.) or an instance of type
369
    ``cls`` if given.
370
    """
371
    jdkwargs = jdkwargs or {}
372
    obj = json.loads(str_, **jdkwargs)
373
    return load(obj, cls, *args, **kwargs)
374
375
376
def dumpb(obj: object, encoding: str = 'utf-8',
377
          jdkwargs: Dict[str, object] = None, *args, **kwargs) -> bytes:
378
    """
379
    Extend ``json.dumps``, allowing any Python instance to be dumped to bytes.
380
    Any extra (keyword) arguments are passed on to ``json.dumps``.
381
382
    :param obj: the object that is to be dumped to bytes.
383
    :param encoding: the encoding that is used to transform to bytes.
384
    :param jdkwargs: extra keyword arguments for ``json.dumps`` (not
385
    ``jsons.dumps``!)
386
    :param args: extra arguments for ``jsons.dumps``.
387
    :param kwargs: Keyword arguments that are passed on through the
388
    serialization process.
389
    passed on to the serializer function.
390
    :return: ``obj`` as ``bytes``.
391
    """
392
    jdkwargs = jdkwargs or {}
393
    dumped_dict = dump(obj, *args, **kwargs)
394
    dumped_str = json.dumps(dumped_dict, **jdkwargs)
395
    return dumped_str.encode(encoding=encoding)
396
397
398
def loadb(bytes_: bytes, cls: type = None, encoding: str = 'utf-8',
399
          jdkwargs: Dict[str, object] = None, *args, **kwargs) -> object:
400
    """
401
    Extend ``json.loads``, allowing bytes to be loaded into a dict or a Python
402
    instance of type ``cls``. Any extra (keyword) arguments are passed on to
403
    ``json.loads``.
404
405
    :param bytes_: the bytes that are to be loaded.
406
    :param cls: a matching class of which an instance should be returned.
407
    :param encoding: the encoding that is used to transform from bytes.
408
    :param jdkwargs: extra keyword arguments for ``json.loads`` (not
409
    ``jsons.loads``!)
410
    :param args: extra arguments for ``jsons.loads``.
411
    :param kwargs: extra keyword arguments for ``jsons.loads``.
412
    :return: a JSON-type object (dict, str, list, etc.) or an instance of type
413
    ``cls`` if given.
414
    """
415
    jdkwargs = jdkwargs or {}
416
    str_ = bytes_.decode(encoding=encoding)
417
    return loads(str_, cls, jdkwargs=jdkwargs, *args, **kwargs)
418
419
420
def set_serializer(func: callable, cls: type, high_prio: bool = True,
421
                   fork_inst: type = JsonSerializable) -> None:
422
    """
423
    Set a serializer function for the given type. You may override the default
424
    behavior of ``jsons.load`` by setting a custom serializer.
425
426
    The ``func`` argument must take one argument (i.e. the object that is to be
427
    serialized) and also a ``kwargs`` parameter. For example:
428
429
    >>> def func(obj, **kwargs):
430
    ...    return dict()
431
432
    You may ask additional arguments between ``cls`` and ``kwargs``.
433
434
    :param func: the serializer function.
435
    :param cls: the type this serializer can handle.
436
    :param high_prio: determines the order in which is looked for the callable.
437
    :param fork_inst: if given, it uses this fork of ``JsonSerializable``.
438
    :return: None.
439
    """
440
    if cls:
441
        index = 0 if high_prio else len(fork_inst._classes_serializers)
442
        fork_inst._classes_serializers.insert(index, cls)
443
        fork_inst._serializers[cls.__name__.lower()] = func
444
    else:
445
        fork_inst._serializers['nonetype'] = func
446
447
448
def set_deserializer(func: callable, cls: type, high_prio: bool = True,
449
                     fork_inst: type = JsonSerializable) -> None:
450
    """
451
    Set a deserializer function for the given type. You may override the
452
    default behavior of ``jsons.dump`` by setting a custom deserializer.
453
454
    The ``func`` argument must take two arguments (i.e. the dict containing the
455
    serialized values and the type that the values should be deserialized into)
456
    and also a ``kwargs`` parameter. For example:
457
458
    >>> def func(dict_, cls, **kwargs):
459
    ...    return cls()
460
461
    You may ask additional arguments between ``cls`` and ``kwargs``.
462
463
    :param func: the deserializer function.
464
    :param cls: the type this serializer can handle.
465
    :param high_prio: determines the order in which is looked for the callable.
466
    :param fork_inst: if given, it uses this fork of ``JsonSerializable``.
467
    :return: None.
468
    """
469
    if cls:
470
        index = 0 if high_prio else len(fork_inst._classes_deserializers)
471
        fork_inst._classes_deserializers.insert(index, cls)
472
        fork_inst._deserializers[cls.__name__.lower()] = func
473
    else:
474
        fork_inst._deserializers['nonetype'] = func
475
476
477
def camelcase(str_: str) -> str:
478
    """
479
    Return ``s`` in camelCase.
480
    :param str_: the string that is to be transformed.
481
    :return: a string in camelCase.
482
    """
483
    str_ = str_.replace('-', '_')
484
    splitted = str_.split('_')
485
    if len(splitted) > 1:
486
        str_ = ''.join([x.title() for x in splitted])
487
    return str_[0].lower() + str_[1:]
488
489
490
def snakecase(str_: str) -> str:
491
    """
492
    Return ``s`` in snake_case.
493
    :param str_: the string that is to be transformed.
494
    :return: a string in snake_case.
495
    """
496
    str_ = str_.replace('-', '_')
497
    str_ = str_[0].lower() + str_[1:]
498
    return re.sub(r'([a-z])([A-Z])', '\\1_\\2', str_).lower()
499
500
501
def pascalcase(str_: str) -> str:
502
    """
503
    Return ``s`` in PascalCase.
504
    :param str_: the string that is to be transformed.
505
    :return: a string in PascalCase.
506
    """
507
    camelcase_str = camelcase(str_)
508
    return camelcase_str[0].upper() + camelcase_str[1:]
509
510
511
def lispcase(str_: str) -> str:
512
    """
513
    Return ``s`` in lisp-case.
514
    :param str_: the string that is to be transformed.
515
    :return: a string in lisp-case.
516
    """
517
    return snakecase(str_).replace('_', '-')
518