Completed
Push — master ( 87b7dd...8c4489 )
by Ramon
12s
created

jsons._main_impl.load()   B

Complexity

Conditions 8

Size

Total Lines 75
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

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