_get_value_for_attr()   B
last analyzed

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__, fallback_ns=cls.__module__)
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
116
def _remove_prefix(prefix: str, s: str) -> str:
117
    if s.startswith(prefix):
118
        return s[len(prefix):] or '/'  # Special case: map the empty string to '/'
119
    return s
120
121
122
def _get_value_from_obj(obj, cls, sig, sig_key, meta_hints, **kwargs):
123
    # Obtain the value for the attribute with the given signature from the
124
    # given obj. Try to obtain the class of this attribute from the meta info
125
    # or from type hints.
126
    cls_key = '/{}'.format(sig_key)
127
    cls_str_from_meta = meta_hints.get(cls_key, None)
128
    new_hints = meta_hints
129
    cls_from_meta = None
130
    if cls_str_from_meta:
131
        cls_from_meta = get_cls_from_str(
132
            cls_str_from_meta, obj, kwargs['fork_inst'])
133
        # Rebuild the class hints: cls_key becomes the new root.
134
        new_hints = {
135
            _remove_prefix(cls_key, key): meta_hints[key]
136
            for key in meta_hints
137
        }
138
    cls_ = determine_precedence(cls=cls, cls_from_meta=cls_from_meta,
139
                                cls_from_type=None, inferred_cls=True)
140
    value = load(obj[sig_key], cls_, meta_hints=new_hints, **kwargs)
141
    return value
142
143
144
def _set_remaining_attrs(instance,
145
                         remaining_attrs,
146
                         attr_getters,
147
                         **kwargs):
148
    # Set any remaining attributes on the newly created instance.
149
    attr_getters = attr_getters or {}
150
    for attr_name in remaining_attrs:
151
        annotations = get_type_hints(instance.__class__)
152
        attr_type = annotations.get(attr_name)
153
154
        if isinstance(remaining_attrs[attr_name], dict) \
155
                and '-keys' in remaining_attrs[attr_name] \
156
                and not attr_type:
157
            fork_inst = kwargs['fork_inst']
158
            fork_inst._warn('A dict with -keys was detected without a type '
159
                            'hint for attribute `{}`. This probably means '
160
                            'that you did not provide an annotation in your '
161
                            'class (ending up in __annotations__).'
162
                            .format(attr_name), 'hashed-keys-without-hint')
163
        attr_type = attr_type or type(remaining_attrs[attr_name])
164
165
        loaded_attr = load(remaining_attrs[attr_name], attr_type, **kwargs)
166
        try:
167
            setattr(instance, attr_name, loaded_attr)
168
        except AttributeError:
169
            pass  # This is raised when a @property does not have a setter.
170
    for attr_name, getter in attr_getters.items():
171
        setattr(instance, attr_name, getter())
172
173
174
def _check_and_transform_keys(obj: dict,
175
                              key_transformer: Optional[Callable[[str], str]],
176
                              **kwargs) -> Tuple[dict, dict]:
177
    if key_transformer:
178
        obj = {key_transformer(key): obj[key] for key in obj}
179
        kwargs = {
180
            **kwargs,
181
            'key_transformer': key_transformer
182
        }
183
    return obj, kwargs
184
185
186
def _get_remaining_args(obj: dict,
187
                        cls: type,
188
                        constructor_args: dict,
189
                        strict: bool,
190
                        fork_inst: type) -> dict:
191
    # Get the remaining args or raise if strict and the signature is unmatched.
192
    remaining_attrs = {attr_name: obj[attr_name] for attr_name in obj
193
                       if attr_name not in constructor_args
194
                       and attr_name != META_ATTR}
195
    if strict and remaining_attrs:
196
        unexpected_arg = list(remaining_attrs.keys())[0]
197
        err_msg = ('Type "{}" does not expect "{}".'
198
                   .format(get_class_name(cls), unexpected_arg))
199
        raise SignatureMismatchError(err_msg, unexpected_arg, obj, cls)
200
    return remaining_attrs
201