Passed
Push — master ( 988c2c...4e149e )
by Ramon
01:10
created

default_object_deserializer()   A

Complexity

Conditions 1

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 28
rs 9.7
c 0
b 0
f 0
cc 1
nop 5
1
import inspect
2
from functools import partial
3
from typing import Optional, Callable, Tuple
4
from jsons._common_impl import get_class_name, META_ATTR, get_cls_from_str
5
from jsons._main_impl import load
6
from jsons.exceptions import SignatureMismatchError, UnfulfilledArgumentError
7
8
9
def default_object_deserializer(
10
        obj: dict,
11
        cls: type,
12
        key_transformer: Optional[Callable[[str], str]] = None,
13
        strict: bool = False,
14
        **kwargs) -> object:
15
    """
16
    Deserialize ``obj`` into an instance of type ``cls``. If ``obj`` contains
17
    keys with a certain case style (e.g. camelCase) that do not match the style
18
    of ``cls`` (e.g. snake_case), a key_transformer should be used (e.g.
19
    KEY_TRANSFORMER_SNAKECASE).
20
    :param obj: a serialized instance of ``cls``.
21
    :param cls: the type to which ``obj`` should be deserialized.
22
    :param key_transformer: a function that transforms the keys in order to
23
    match the attribute names of ``cls``.
24
    :param strict: deserialize in strict mode.
25
    :param kwargs: any keyword arguments that may be passed to the
26
    deserializers.
27
    :return: an instance of type ``cls``.
28
    """
29
    obj, kwargs = _check_and_transform_keys(obj, key_transformer, **kwargs)
30
    kwargs['strict'] = strict
31
    constructor_args = _get_constructor_args(obj, cls, **kwargs)
32
    remaining_attrs = _get_remaining_args(obj, cls, constructor_args,
33
                                          strict, kwargs['fork_inst'])
34
    instance = cls(**constructor_args)
35
    _set_remaining_attrs(instance, remaining_attrs, **kwargs)
36
    return instance
37
38
39
def _get_constructor_args(
40
        obj,
41
        cls,
42
        meta_hints,
43
        attr_getters=None,
44
        **kwargs) -> dict:
45
    # Loop through the signature of cls: the type we try to deserialize to. For
46
    # every required parameter, we try to get the corresponding value from
47
    # json_obj.
48
    signature_parameters = inspect.signature(cls.__init__).parameters
49
    attr_getters = dict(**(attr_getters or {}))
50
    value_for_attr_part = partial(_get_value_for_attr,
51
                                  obj=obj,
52
                                  cls=cls,
53
                                  meta_hints=meta_hints,
54
                                  attr_getters=attr_getters,
55
                                  **kwargs)
56
    args_gen = (value_for_attr_part(sig_key=sig_key, sig=sig) for sig_key, sig
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable sig does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable sig_key does not seem to be defined.
Loading history...
57
                in signature_parameters.items() if sig_key != 'self')
58
    constructor_args_in_obj = {key: value for key, value in args_gen if key}
59
    return constructor_args_in_obj
60
61
62
def _get_value_for_attr(
63
        obj,
64
        cls,
65
        sig_key,
66
        sig,
67
        meta_hints,
68
        attr_getters,
69
        **kwargs):
70
    # Find a value for the attribute (with signature sig_key).
71
    result = None, None
72
    if obj and sig_key in obj:
73
        # This argument is in obj.
74
        result = sig_key, _get_value_from_obj(obj, sig, sig_key,
75
                                              meta_hints, **kwargs)
76
    elif sig_key in attr_getters:
77
        # There exists an attr_getter for this argument.
78
        attr_getter = attr_getters.pop(sig_key)
79
        result = sig_key, attr_getter()
80
    elif sig.default != inspect.Parameter.empty:
81
        # There is a default value for this argument.
82
        result = sig_key, sig.default
83
    elif sig.kind not in (inspect.Parameter.VAR_POSITIONAL,
84
                          inspect.Parameter.VAR_KEYWORD):
85
        # This argument is no *args or **kwargs and has no value.
86
        raise UnfulfilledArgumentError(
87
            'No value found for "{}"'.format(sig_key), sig_key, obj, cls)
88
    return result
89
90
91
def _get_value_from_obj(obj, sig, sig_key, meta_hints, **kwargs):
92
    # Obtain the value for the attribute with the given signature from the
93
    # given obj. Try to obtain the class of this attribute from the meta info
94
    # or from type hints.
95
    cls_key = '/{}'.format(sig_key)
96
    cls_from_meta = meta_hints.get(cls_key, None)
97
    new_hints = meta_hints
98
    arg_cls = None
99
    if cls_from_meta:
100
        arg_cls = get_cls_from_str(cls_from_meta, obj, kwargs['fork_inst'])
101
        # Rebuild the class hints: cls_key becomes the new root.
102
        new_hints = {
103
            key.replace(cls_key, '/'): meta_hints[key]
104
            for key in meta_hints
105
            if key != '/'
106
        }
107
    elif sig.annotation != inspect.Parameter.empty:
108
        arg_cls = sig.annotation
109
    value = load(obj[sig_key], arg_cls, meta_hints=new_hints, **kwargs)
110
    return value
111
112
113
def _set_remaining_attrs(instance,
114
                         remaining_attrs,
115
                         attr_getters=None,
116
                         **kwargs):
117
    # Set any remaining attributes on the newly created instance.
118
    attr_getters = attr_getters or {}
119
    for attr_name in remaining_attrs:
120
        loaded_attr = load(remaining_attrs[attr_name],
121
                           type(remaining_attrs[attr_name]),
122
                           **kwargs)
123
        try:
124
            setattr(instance, attr_name, loaded_attr)
125
        except AttributeError:
126
            pass  # This is raised when a @property does not have a setter.
127
    for attr_name, getter in attr_getters.items():
128
        setattr(instance, attr_name, getter())
129
130
131
def _check_and_transform_keys(obj: dict,
132
                              key_transformer: Optional[Callable[[str], str]],
133
                              **kwargs) -> Tuple[dict, dict]:
134
    if key_transformer:
135
        obj = {key_transformer(key): obj[key] for key in obj}
136
        kwargs = {
137
            **kwargs,
138
            'key_transformer': key_transformer
139
        }
140
    return obj, kwargs
141
142
143
def _get_remaining_args(obj: dict,
144
                        cls: type,
145
                        constructor_args: dict,
146
                        strict: bool,
147
                        fork_inst: type) -> dict:
148
    # Get the remaining args or raise if strict and the signature is unmatched.
149
    remaining_attrs = {attr_name: obj[attr_name] for attr_name in obj
150
                       if attr_name not in constructor_args
151
                       and attr_name != META_ATTR}
152
    if strict and remaining_attrs:
153
        unexpected_arg = list(remaining_attrs.keys())[0]
154
        err_msg = ('Type "{}" does not expect "{}"'
155
                   .format(get_class_name(cls, fork_inst=fork_inst),
156
                           unexpected_arg))
157
        raise SignatureMismatchError(err_msg, unexpected_arg, obj, cls)
158
    return remaining_attrs
159