Passed
Branch master (272386)
by Ramon
01:04
created

jsons._common_impl.lispcase()   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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