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__, 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
            if key != '/'
138
        }
139
    cls_ = determine_precedence(cls=cls, cls_from_meta=cls_from_meta,
140
                                cls_from_type=None, inferred_cls=True)
141
    value = load(obj[sig_key], cls_, meta_hints=new_hints, **kwargs)
142
    return value
143
144
145
def _set_remaining_attrs(instance,
146
                         remaining_attrs,
147
                         attr_getters=None,
148
                         **kwargs):
149
    # Set any remaining attributes on the newly created instance.
150
    attr_getters = attr_getters or {}
151
    for attr_name in remaining_attrs:
152
        loaded_attr = load(remaining_attrs[attr_name],
153
                           type(remaining_attrs[attr_name]),
154
                           **kwargs)
155
        try:
156
            setattr(instance, attr_name, loaded_attr)
157
        except AttributeError:
158
            pass  # This is raised when a @property does not have a setter.
159
    for attr_name, getter in attr_getters.items():
160
        setattr(instance, attr_name, getter())
161
162
163
def _check_and_transform_keys(obj: dict,
164
                              key_transformer: Optional[Callable[[str], str]],
165
                              **kwargs) -> Tuple[dict, dict]:
166
    if key_transformer:
167
        obj = {key_transformer(key): obj[key] for key in obj}
168
        kwargs = {
169
            **kwargs,
170
            'key_transformer': key_transformer
171
        }
172
    return obj, kwargs
173
174
175
def _get_remaining_args(obj: dict,
176
                        cls: type,
177
                        constructor_args: dict,
178
                        strict: bool,
179
                        fork_inst: type) -> dict:
180
    # Get the remaining args or raise if strict and the signature is unmatched.
181
    remaining_attrs = {attr_name: obj[attr_name] for attr_name in obj
182
                       if attr_name not in constructor_args
183
                       and attr_name != META_ATTR}
184
    if strict and remaining_attrs:
185
        unexpected_arg = list(remaining_attrs.keys())[0]
186
        err_msg = ('Type "{}" does not expect "{}".'
187
                   .format(get_class_name(cls), unexpected_arg))
188
        raise SignatureMismatchError(err_msg, unexpected_arg, obj, cls)
189
    return remaining_attrs
190