Completed
Push — master ( 86f2a1...1d3482 )
by Ramon
01:04
created

jsons.JsonSerializable.json()   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 1
nop 1
1
"""
2
Works with Python3.5+
3
4
JSON (de)serialization (jsons) from and to dicts and plain old Python objects.
5
6
Works with dataclasses (Python3.7+).
7
8
9
**Example:**
10
11
    >>> from dataclasses import dataclass
12
    >>> @dataclass
13
    ... class Car:
14
    ...     color: str
15
    >>> @dataclass
16
    ... class Person:
17
    ...     car: Car
18
    ...     name: str
19
    >>> c = Car('Red')
20
    >>> p = Person(c, 'John')
21
    >>> dumped = dump(p)
22
    >>> dumped['name']
23
    'John'
24
    >>> dumped['car']['color']
25
    'Red'
26
    >>> p_reloaded = load(dumped, Person)
27
    >>> p_reloaded.name
28
    'John'
29
    >>> p_reloaded.car.color
30
    'Red'
31
32
33
Deserialization will work with older Python classes (Python3.5+) given that
34
type hints are present for custom types (i.e. any type that is not set at
35
the bottom of this module). Serialization will work with no type hints at
36
all.
37
38
39
**Example**
40
41
    >>> class Car:
42
    ...     def __init__(self, color):
43
    ...         self.color = color
44
    >>> class Person:
45
    ...     def __init__(self, car: Car, name):
46
    ...         self.car = car
47
    ...         self.name = name
48
    >>> c = Car('Red')
49
    >>> p = Person(c, 'John')
50
    >>> dumped = dump(p)
51
    >>> dumped['name']
52
    'John'
53
    >>> dumped['car']['color']
54
    'Red'
55
    >>> p_reloaded = load(dumped, Person)
56
    >>> p_reloaded.name
57
    'John'
58
    >>> p_reloaded.car.color
59
    'Red'
60
61
62
Alternatively, you can make use of the `JsonSerializable` class.
63
64
65
**Example**
66
67
    >>> class Car(JsonSerializable):
68
    ...     def __init__(self, color):
69
    ...         self.color = color
70
    >>> class Person(JsonSerializable):
71
    ...     def __init__(self, car: Car, name):
72
    ...         self.car = car
73
    ...         self.name = name
74
    >>> c = Car('Red')
75
    >>> p = Person(c, 'John')
76
    >>> dumped = p.json
77
    >>> dumped['name']
78
    'John'
79
    >>> dumped['car']['color']
80
    'Red'
81
    >>> p_reloaded = Person.from_json(dumped)
82
    >>> p_reloaded.name
83
    'John'
84
    >>> p_reloaded.car.color
85
    'Red'
86
87
"""
88
89
import inspect
90
import json
91
import re
92
from datetime import datetime, timedelta, timezone
93
from enum import Enum, EnumMeta
94
from typing import List
95
96
_JSON_TYPES = (str, int, float, bool)
97
_RFC3339_DATETIME_PATTERN = '%Y-%m-%dT%H:%M:%S'
98
_CLASSES = list()
99
_SERIALIZERS = dict()
100
_DESERIALIZERS = dict()
101
102
103
def dump(obj: object) -> dict:
104
    """
105
    Serialize the given ``obj`` to a dict.
106
107
    The way objects are serialized can be finetuned by setting serializer
108
    functions for the specific type using ``set_serializer``.
109
    :param obj: a Python instance of any sort.
110
    :return: the serialized obj as a dict.
111
    """
112
    serializer = _SERIALIZERS.get(obj.__class__.__name__, None)
113
    if not serializer:
114
        parents = [cls for cls in _CLASSES if isinstance(obj, cls)]
115
        if parents:
116
            serializer = _SERIALIZERS[parents[0].__name__]
117
    return serializer(obj)
118
119
120
def load(json_obj: dict, cls: type = None) -> object:
121
    """
122
    Deserialize the given ``json_obj`` to an object of type ``cls``. If the
123
    contents of ``json_obj`` do not match the interface of ``cls``, a
124
    TypeError is raised.
125
126
    If ``json_obj`` contains a value that belongs to a custom class, there must
127
    be a type hint present for that value in ``cls`` to let this function know
128
    what type it should deserialize that value to.
129
130
131
    **Example**:
132
133
        ``class Person:``
134
            ``# No type hint required for name``
135
136
        ``class Person:``
137
            ``# No type hint required for name``
138
139
            ``def __init__(self, name):``
140
                ``self.name = name``
141
        ````
142
        ``class Family:``
143
            ``# Person is a custom class, use a type hint``
144
145
            ``def __init__(self, persons: List[Person]):``
146
                ``self.persons = persons``
147
148
        ``jsons.load(some_dict, Family)``
149
150
    If no ``cls`` is given, a dict is simply returned, but contained values
151
    (e.g. serialized ``datetime`` values) are still deserialized.
152
    :param json_obj: the dict that is to be deserialized.
153
    :param cls: a matching class of which an instance should be returned.
154
    :return: an instance of ``cls`` if given, a dict otherwise.
155
    """
156
    cls = cls or type(json_obj)
157
    cls_name = cls.__name__ if hasattr(cls, '__name__') \
158
        else cls.__origin__.__name__
159
    deserializer = _DESERIALIZERS.get(cls_name, None)
160
    if not deserializer:
161
        parents = [cls_ for cls_ in _CLASSES if issubclass(cls, cls_)]
162
        if parents:
163
            deserializer = _DESERIALIZERS[parents[0].__name__]
164
    return deserializer(json_obj, cls)
165
166
167
def dumps(obj: object, *args, **kwargs) -> str:
168
    """
169
    Extend ``json.dumps``, allowing any Python instance to be dumped to a
170
    string. Any extra (keyword) arguments are passed on to ``json.dumps``.
171
172
    :param obj: the object that is to be dumped to a string.
173
    :param args: extra arguments for ``json.dumps``.
174
    :param kwargs: extra keyword arguments for ``json.dumps``.
175
    :return: ``obj`` as a ``str``.
176
    """
177
    return json.dumps(dump(obj), *args, **kwargs)
178
179
180
def loads(s: str, cls: type = None, *args, **kwargs) -> object:
181
    """
182
    Extend ``json.loads``, allowing a string to be loaded into a dict or a
183
    Python instance of type ``cls``. Any extra (keyword) arguments are passed
184
    on to ``json.loads``.
185
186
    :param s: the string that is to be loaded.
187
    :param cls: a matching class of which an instance should be returned.
188
    :param args: extra arguments for ``json.dumps``.
189
    :param kwargs: extra keyword arguments for ``json.dumps``.
190
    :return: an instance of type ``cls`` or a dict if no ``cls`` is given.
191
    """
192
    obj = json.loads(s, *args, **kwargs)
193
    return load(obj, cls) if cls else obj
194
195
196
def set_serializer(c: callable, cls: type) -> None:
197
    """
198
    Set a serializer function for the given type. You may override the default
199
    behavior of ``jsons.load`` by setting a custom serializer.
200
201
    :param c: the serializer function.
202
    :param cls: the type this serializer can handle.
203
    :return: None.
204
    """
205
    if cls:
206
        _CLASSES.insert(0, cls)
207
        _SERIALIZERS[cls.__name__] = c
208
    else:
209
        _SERIALIZERS['NoneType'] = c
210
211
212
def set_deserializer(c: callable, cls: type) -> None:
213
    """
214
    Set a deserializer function for the given type. You may override the
215
    default behavior of ``jsons.dump`` by setting a custom deserializer.
216
217
    :param c: the deserializer function.
218
    :param cls: the type this serializer can handle.
219
    :return: None.
220
    """
221
    if cls:
222
        _CLASSES.insert(0, cls)
223
        _DESERIALIZERS[cls.__name__] = c
224
    else:
225
        _DESERIALIZERS['NoneType'] = c
226
227
228
class JsonSerializable:
229
    """
230
    This class offers an alternative to using the `jsons.load` and `jsons.dump`
231
    methods. An instance of a class that inherits from JsonSerializable has the
232
    `json` property, which value is equivalent to calling `jsons.dump` on that
233
    instance. Furthermore, you can call `from_json` on that class, which is
234
    equivalent to calling `json.load` with that class as an argument.
235
    """
236
    @property
237
    def json(self) -> dict:
238
        """
239
        See `jsons.dump`.
240
        :return: this instance in a JSON representation (dict).
241
        """
242
        return dump(self)
243
244
    @classmethod
245
    def from_json(cls, json_obj: dict) -> object:
246
        """
247
        See `jsons.load`.
248
        :param json_obj: a JSON representation of an instance of the inheriting
249
        class
250
        :return: an instance of the inheriting class.
251
        """
252
        return load(json_obj, cls)
253
254
255
#######################
256
# Default serializers #
257
#######################
258
259
def _default_list_serializer(obj: list) -> dict:
260
    return [dump(elem) for elem in obj]
261
262
263
def _default_enum_serializer(obj: EnumMeta) -> dict:
264
    return obj.name
265
266
267
def _default_datetime_serializer(obj: datetime) -> dict:
268
    timezone = obj.tzinfo
269
    offset = 'Z'
270
    pattern = _RFC3339_DATETIME_PATTERN
271
    if timezone:
272
        if timezone.tzname(None) != 'UTC':
273
            offset = timezone.tzname(None).split('UTC')[1]
274
    if obj.microsecond:
275
        pattern += '.%f'
276
    return obj.strftime("{}{}".format(pattern, offset))
277
278
279
def _default_primitive_serializer(obj) -> dict:
280
    return obj
281
282
283
def _default_object_serializer(obj) -> dict:
284
    d = obj.__dict__
285
    return {key: dump(d[key]) for key in d}
286
287
288
#########################
289
# Default deserializers #
290
#########################
291
292
def _default_datetime_deserializer(obj: str, _: datetime) -> datetime:
293
    pattern = _RFC3339_DATETIME_PATTERN
294
    if '.' in obj:
295
        pattern += '.%f'
296
        # strptime allows a fraction of length 6, so trip the rest (if exists).
297
        regex_pattern = re.compile('(\.[0-9]+)')
298
        frac = regex_pattern.search(obj).group()
299
        obj = obj.replace(frac, frac[0:7])
300
    if obj[-1] == 'Z':
301
        dattim_str = obj[0:-1]
302
        dattim_obj = datetime.strptime(dattim_str, pattern)
303
    else:
304
        dattim_str, offset = obj.split('+')
305
        dattim_obj = datetime.strptime(dattim_str, pattern)
306
        hours, minutes = offset.split(':')
307
        tz = timezone(offset=timedelta(hours=int(hours), minutes=int(minutes)))
308
        datetime_list = [dattim_obj.year, dattim_obj.month, dattim_obj.day,
309
                         dattim_obj.hour, dattim_obj.minute, dattim_obj.second,
310
                         dattim_obj.microsecond, tz]
311
        dattim_obj = datetime(*datetime_list)
312
    return dattim_obj
313
314
315
def _default_object_deserializer(obj: dict, cls: type) -> object:
316
    signature_parameters = inspect.signature(cls.__init__).parameters
317
    # Loop through the signature of cls: the type we try to deserialize to. For
318
    # every required parameter, we try to get the corresponding value from
319
    # json_obj.
320
    constructor_args = dict()
321
    for signature_key, signature in signature_parameters.items():
322
        if obj and signature_key is not 'self':
323
            if signature_key in obj:
324
                cls_ = None
325
                if signature.annotation != inspect._empty:
326
                    cls_ = signature.annotation
327
                value = load(obj[signature_key], cls_)
328
                constructor_args[signature_key] = value
329
330
    # The constructor arguments are gathered, create an instance.
331
    instance = cls(**constructor_args)
332
    # Set any remaining attributes on the newly created instance.
333
    remaining_attrs = {attr_name: obj[attr_name] for attr_name in obj
334
                       if attr_name not in constructor_args}
335
    for attr_name in remaining_attrs:
336
        loaded_attr = load(remaining_attrs[attr_name],
337
                           type(remaining_attrs[attr_name]))
338
        setattr(instance, attr_name, loaded_attr)
339
    return instance
340
341
342
def _default_list_deserializer(obj: List, cls) -> object:
343
    cls_ = None
344
    if cls and hasattr(cls, '__args__'):
345
        cls_ = cls.__args__[0]
346
    return [load(x, cls_) for x in obj]
347
348
349
def _default_enum_deserializer(obj: Enum, cls: EnumMeta) -> object:
350
    return cls[obj]
351
352
353
def _default_string_deserializer(obj: str, _: type = None) -> object:
354
    try:
355
        return load(obj, datetime)
356
    except:
357
        return obj
358
359
360
def _default_primitive_deserializer(obj: object, _: type = None) -> object:
361
    return obj
362
363
364
# The order of the below is important.
365
set_serializer(_default_object_serializer, object)
366
set_serializer(_default_list_serializer, list)
367
set_serializer(_default_enum_serializer, Enum)
368
set_serializer(_default_datetime_serializer, datetime)
369
set_serializer(_default_primitive_serializer, str)
370
set_serializer(_default_primitive_serializer, int)
371
set_serializer(_default_primitive_serializer, float)
372
set_serializer(_default_primitive_serializer, bool)
373
set_serializer(_default_primitive_serializer, dict)
374
set_serializer(_default_primitive_serializer, None)
375
set_deserializer(_default_object_deserializer, object)
376
set_deserializer(_default_list_deserializer, list)
377
set_deserializer(_default_enum_deserializer, Enum)
378
set_deserializer(_default_datetime_deserializer, datetime)
379
set_deserializer(_default_string_deserializer, str)
380
set_deserializer(_default_primitive_deserializer, int)
381
set_deserializer(_default_primitive_deserializer, float)
382
set_deserializer(_default_primitive_deserializer, bool)
383
set_deserializer(_default_primitive_deserializer, dict)
384
set_deserializer(_default_primitive_deserializer, None)
385