Completed
Push — master ( 956350...005733 )
by Ramon
01:19
created

jsons.deserializers.default_object_deserializer()   D

Complexity

Conditions 12

Size

Total Lines 47
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 28
nop 4
dl 0
loc 47
rs 4.8
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like jsons.deserializers.default_object_deserializer() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""
0 ignored issues
show
Bug introduced by
There seems to be a cyclic import (jsons -> jsons.serializers).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
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
    if key_transformer:
165
        obj = {key_transformer(key): obj[key] for key in obj}
166
    signature_parameters = inspect.signature(cls.__init__).parameters
167
    # Loop through the signature of cls: the type we try to deserialize to. For
168
    # every required parameter, we try to get the corresponding value from
169
    # json_obj.
170
    constructor_args = dict()
171
    for signature_key, signature in signature_parameters.items():
172
        if obj and signature_key != 'self':
173
            if signature_key in obj:
174
                cls_ = None
175
                if signature.annotation != inspect._empty:
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...
176
                    cls_ = signature.annotation
177
                value = load_impl(obj[signature_key], cls_,
178
                                  key_transformer=key_transformer, **kwargs)
179
                constructor_args[signature_key] = value
180
181
    # The constructor arguments are gathered, create an instance.
182
    instance = cls(**constructor_args)
183
    # Set any remaining attributes on the newly created instance.
184
    remaining_attrs = {attr_name: obj[attr_name] for attr_name in obj
185
                       if attr_name not in constructor_args}
186
    for attr_name in remaining_attrs:
187
        loaded_attr = load_impl(remaining_attrs[attr_name],
188
                                type(remaining_attrs[attr_name]),
189
                                key_transformer=key_transformer, **kwargs)
190
        try:
191
            setattr(instance, attr_name, loaded_attr)
192
        except AttributeError:
193
            pass  # This is raised when a @property does not have a setter.
194
    return instance
195
196
197
# The following default key transformers can be used with the
198
# default_object_serializer.
199
KEY_TRANSFORMER_SNAKECASE = snakecase
200
KEY_TRANSFORMER_CAMELCASE = camelcase
201
KEY_TRANSFORMER_PASCALCASE = pascalcase
202
KEY_TRANSFORMER_LISPCASE = lispcase
203