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