Passed
Push — master ( 005733...10033b )
by Ramon
01:08
created

default_datetime_deserializer()   A

Complexity

Conditions 3

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 19
nop 3
dl 0
loc 28
rs 9.45
c 0
b 0
f 0
1
"""
2
This module contains default deserializers. You can override the
3
deserialization process of a particular type as follows:
4
5
``jsons.set_deserializer(custom_deserializer, SomeClass)``
6
"""
7
import inspect
8
import re
9
from datetime import datetime, timezone, timedelta
10
from enum import EnumMeta
11
from typing import List, Callable
12
from jsons._common_impl import RFC3339_DATETIME_PATTERN, load_impl, \
13
    snakecase, camelcase, pascalcase, lispcase
14
15
16
def default_datetime_deserializer(obj: str, _: datetime, **__) -> datetime:
17
    """
18
    Deserialize a string with an RFC3339 pattern to a datetime instance.
19
    :param obj:
20
    :param _: not used.
21
    :param __: not used.
22
    :return: a ``datetime.datetime`` instance.
23
    """
24
    pattern = RFC3339_DATETIME_PATTERN
25
    if '.' in obj:
26
        pattern += '.%f'
27
        # strptime allows a fraction of length 6, so trip the rest (if exists).
28
        regex_pattern = re.compile(r'(\.[0-9]+)')
29
        frac = regex_pattern.search(obj).group()
30
        obj = obj.replace(frac, frac[0:7])
31
    if obj[-1] == 'Z':
32
        dattim_str = obj[0:-1]
33
        dattim_obj = datetime.strptime(dattim_str, pattern)
34
    else:
35
        dattim_str, offset = obj.split('+')
36
        dattim_obj = datetime.strptime(dattim_str, pattern)
37
        hours, minutes = offset.split(':')
38
        tz = timezone(offset=timedelta(hours=int(hours), minutes=int(minutes)))
0 ignored issues
show
Coding Style Naming introduced by
The name tz does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
39
        datetime_list = [dattim_obj.year, dattim_obj.month, dattim_obj.day,
40
                         dattim_obj.hour, dattim_obj.minute, dattim_obj.second,
41
                         dattim_obj.microsecond, tz]
42
        dattim_obj = datetime(*datetime_list)
43
    return dattim_obj
44
45
46
def default_list_deserializer(obj: List, cls, **kwargs) -> object:
47
    """
48
    Deserialize a list by deserializing all items of that list.
49
    :param obj: the list that needs deserializing.
50
    :param cls: the type with a generic (e.g. List[str]).
51
    :param kwargs: any keyword arguments.
52
    :return: a deserialized list instance.
53
    """
54
    cls_ = None
55
    if cls and hasattr(cls, '__args__'):
56
        cls_ = cls.__args__[0]
57
    return [load_impl(x, cls_, **kwargs) for x in obj]
58
59
60
def default_tuple_deserializer(obj: List, cls, **kwargs) -> object:
61
    """
62
    Deserialize a (JSON) list into a tuple by deserializing all items of that
63
    list.
64
    :param obj: the list that needs deserializing.
65
    :param cls: the type with a generic (e.g. Tuple[str, int]).
66
    :param kwargs: any keyword arguments.
67
    :return: a deserialized tuple instance.
68
    """
69
    if hasattr(cls, '__tuple_params__'):
70
        tuple_types = cls.__tuple_params__
71
    else:
72
        tuple_types = cls.__args__
73
    list_ = [load_impl(obj[i], tuple_types[i], **kwargs)
74
             for i in range(len(obj))]
75
    return tuple(list_)
76
77
78
def default_dict_deserializer(obj: dict, _: type,
79
                              key_transformer: Callable[[str], str] = None,
80
                              **kwargs) -> object:
81
    """
82
    Deserialize a dict by deserializing all instances of that dict.
83
    :param obj: the dict that needs deserializing.
84
    :param key_transformer: a function that transforms the keys to a different
85
    style (e.g. PascalCase).
86
    :param cls: not used.
87
    :param kwargs: any keyword arguments.
88
    :return: a deserialized dict instance.
89
    """
90
    key_transformer = key_transformer or (lambda key: key)
91
    new_kwargs = {**{'key_transformer': key_transformer}, **kwargs}
92
    return {key_transformer(key): load_impl(obj[key], **new_kwargs)
93
            for key in obj}
94
95
96
def default_enum_deserializer(obj: str, cls: EnumMeta,
97
                              use_enum_name: bool = True, **__) -> object:
98
    """
99
    Deserialize an enum value to an enum instance. The serialized value must
100
    can be the name of the enum element or the value; dependent on
101
    ``use_enum_name``.
102
    :param obj: the serialized enum.
103
    :param cls: the enum class.
104
    :param use_enum_name: determines whether the name or the value of an enum
105
    element should be used.
106
    :param __: not used.
107
    :return: the corresponding enum element instance.
108
    """
109
    if use_enum_name:
110
        result = cls[obj]
111
    else:
112
        for elem in cls:
113
            if elem.value == obj:
114
                result = elem
115
                break
116
    return result
0 ignored issues
show
introduced by
The variable result does not seem to be defined for all execution paths.
Loading history...
117
118
119
def default_string_deserializer(obj: str, _: type = None, **kwargs) -> object:
120
    """
121
    Deserialize a string. If the given ``obj`` can be parsed to a date, a
122
    ``datetime`` instance is returned.
123
    :param obj: the string that is to be deserialized.
124
    :param _: not used.
125
    :param kwargs: any keyword arguments.
126
    :return: the deserialized obj.
127
    """
128
    try:
129
        # Use load_impl instead of default_datetime_deserializer to allow the
130
        # datetime deserializer to be overridden.
131
        return load_impl(obj, datetime, **kwargs)
132
    except:
0 ignored issues
show
Coding Style Best Practice introduced by
General except handlers without types should be used sparingly.

Typically, you would use general except handlers when you intend to specifically handle all types of errors, f.e. when logging. Otherwise, such general error handlers can mask errors in your application that you want to know of.

Loading history...
133
        return obj
134
135
136
def default_primitive_deserializer(obj: object,
137
                                   _: type = None, **__) -> object:
138
    """
139
    Deserialize a primitive: it simply returns the given primitive.
140
    :param obj: the value that is to be deserialized.
141
    :param _: not used.
142
    :param __: not used.
143
    :return: ``obj``.
144
    """
145
    return obj
146
147
148
def default_object_deserializer(obj: dict, cls: type,
149
                                key_transformer: Callable[[str], str] = None,
150
                                **kwargs) -> object:
151
    """
152
    Deserialize ``obj`` into an instance of type ``cls``. If ``obj`` contains
153
    keys with a certain case style (e.g. camelCase) that do not match the style
154
    of ``cls`` (e.g. snake_case), a key_transformer should be used (e.g.
155
    KEY_TRANSFORMER_SNAKECASE).
156
    :param obj: a serialized instance of ``cls``.
157
    :param cls: the type to which ``obj`` should be deserialized.
158
    :param key_transformer: a function that transforms the keys in order to
159
    match the attribute names of ``cls``.
160
    :param kwargs: any keyword arguments that may be passed to the
161
    deserializers.
162
    :return: an instance of type ``cls``.
163
    """
164
    concat_kwargs = kwargs
165
    if key_transformer:
166
        obj = {key_transformer(key): obj[key] for key in obj}
167
        concat_kwargs = {**kwargs, 'key_transformer': key_transformer}
168
    signature_parameters = inspect.signature(cls.__init__).parameters
169
    constructor_args = _get_constructor_args(obj, signature_parameters,
170
                                             **concat_kwargs)
171
    remaining_attrs = {attr_name: obj[attr_name] for attr_name in obj
172
                       if attr_name not in constructor_args}
173
    instance = cls(**constructor_args)
174
    _set_remaining_attrs(instance, remaining_attrs, **kwargs)
175
    return instance
176
177
178
def _get_constructor_args(obj, signature_parameters, **kwargs):
179
    # Loop through the signature of cls: the type we try to deserialize to. For
180
    # every required parameter, we try to get the corresponding value from
181
    # json_obj.
182
    constructor_args = dict()
183
    sigs = [(sig_key, sig) for sig_key, sig in signature_parameters.items()
184
            if obj and sig_key != 'self' and sig_key in obj]
185
    for sig_key, sig in sigs:
186
        cls = sig.annotation if sig.annotation != inspect._empty else None
1 ignored issue
show
Coding Style Best Practice introduced by
It seems like _empty was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
187
        value = load_impl(obj[sig_key], cls, **kwargs)
188
        constructor_args[sig_key] = value
189
    return constructor_args
190
191
192
def _set_remaining_attrs(instance, remaining_attrs, **kwargs):
193
    # Set any remaining attributes on the newly created instance.
194
    for attr_name in remaining_attrs:
195
        loaded_attr = load_impl(remaining_attrs[attr_name],
196
                                type(remaining_attrs[attr_name]), **kwargs)
197
        try:
198
            setattr(instance, attr_name, loaded_attr)
199
        except AttributeError:
200
            pass  # This is raised when a @property does not have a setter.
201
202
203
# The following default key transformers can be used with the
204
# default_object_serializer.
205
KEY_TRANSFORMER_SNAKECASE = snakecase
206
KEY_TRANSFORMER_CAMELCASE = camelcase
207
KEY_TRANSFORMER_PASCALCASE = pascalcase
208
KEY_TRANSFORMER_LISPCASE = lispcase
209