Passed
Pull Request — master (#65)
by Ramon
01:25
created

_get_value_for_attr()   B

Complexity

Conditions 7

Size

Total Lines 32
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 25
dl 0
loc 32
rs 7.8799
c 0
b 0
f 0
cc 7
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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