Passed
Pull Request — master (#77)
by Ramon
59s
created

  A

Complexity

Conditions 2

Size

Total Lines 24
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 24
rs 9.328
c 0
b 0
f 0
cc 2
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
from datetime import datetime, timezone
2
from inspect import isfunction
3
from typing import Optional, Callable, Union, MutableSequence, Tuple, Dict
4
5
from typish import get_type
6
7
from jsons._cache import cached
8
from jsons._common_impl import get_class_name, META_ATTR
9
from jsons._datetime_impl import to_str
10
from jsons._dump_impl import dump
11
from jsons.classes import JsonSerializable
12
from jsons.classes.verbosity import Verbosity
13
from jsons.exceptions import SerializationError, RecursionDetectedError
14
15
16
def default_object_serializer(
17
        obj: object,
18
        cls: Optional[type] = None,
19
        *,
20
        key_transformer: Optional[Callable[[str], str]] = None,
21
        strip_nulls: bool = False,
22
        strip_privates: bool = False,
23
        strip_properties: bool = False,
24
        strip_class_variables: bool = False,
25
        strip_attr: Union[str, MutableSequence[str], Tuple[str]] = None,
26
        verbose: Union[Verbosity, bool] = False,
27
        strict: bool = False,
28
        **kwargs) -> Optional[dict]:
29
    """
30
    Serialize the given ``obj`` to a dict. All values within ``obj`` are also
31
    serialized. If ``key_transformer`` is given, it will be used to transform
32
    the casing (e.g. snake_case) to a different format (e.g. camelCase).
33
    :param obj: the object that is to be serialized.
34
    :param cls: the type of the object that is to be dumped.
35
    :param key_transformer: a function that will be applied to all keys in the
36
    resulting dict.
37
    :param strip_nulls: if ``True`` the resulting dict will not contain null
38
    values.
39
    :param strip_privates: if ``True`` the resulting dict will not contain
40
    private attributes (i.e. attributes that start with an underscore).
41
    :param strip_properties: if ``True`` the resulting dict will not contain
42
    values from @properties.
43
    :param strip_class_variables: if ``True`` the resulting dict will not
44
    contain values from class variables.
45
    :param strip_attr: can be a name or a collection of names of attributes
46
    that are not to be included in the dump.
47
    dict will not contain attributes with
48
    :param verbose: if ``True`` the resulting dict will contain meta
49
    information (e.g. on how to deserialize).
50
    :param strict: a bool to determine if the serializer should be strict
51
    (i.e. only dumping stuff that is known to ``cls``).
52
    :param kwargs: any keyword arguments that are to be passed to the
53
    serializer functions.
54
    :return: a Python dict holding the values of ``obj``.
55
    """
56
    if obj is None:
57
        return obj
58
    _check_slots(cls, kwargs)
59
    strip_attr = _normalize_strip_attr(strip_attr)
60
    if strict and cls:
61
        attributes = _get_attributes_from_class(
62
            cls, strip_privates, strip_properties, strip_class_variables,
63
            strip_attr)
64
    else:
65
        attributes = _get_attributes_from_object(
66
            obj, strip_privates, strip_properties, strip_class_variables,
67
            strip_attr)
68
        cls = obj.__class__
69
    verbose = Verbosity.from_value(verbose)
70
    kwargs_ = {
71
        **kwargs,
72
        'verbose': verbose,
73
        'strict': strict,
74
        # Set a flag in kwargs to temporarily store -cls:
75
        '_store_cls': Verbosity.WITH_CLASS_INFO in verbose
76
    }
77
78
    result = dict()
79
    fork_inst = kwargs['fork_inst']
80
    for attr_name, cls_ in attributes.items():
81
        attr = obj.__getattribute__(attr_name)
82
        dumped_elem = None
83
        try:
84
            dumped_elem = dump(attr,
85
                               cls=cls_,
86
                               key_transformer=key_transformer,
87
                               strip_nulls=strip_nulls,
88
                               strip_privates=strip_privates,
89
                               strip_properties=strip_properties,
90
                               strip_class_variables=strip_class_variables,
91
                               strip_attr=strip_attr,
92
                               **kwargs_)
93
94
            _store_cls_info(dumped_elem, attr, kwargs_)
95
        except RecursionDetectedError:
96
            fork_inst._warn('Recursive structure detected in attribute "{}" '
97
                            'of object of type "{}", ignoring the attribute.'
98
                            .format(attr_name, get_class_name(cls)))
99
        except SerializationError as err:
100
            if strict:
101
                raise
102
            else:
103
                fork_inst._warn('Failed to dump attribute "{}" of object of '
104
                                'type "{}". Reason: {}. Ignoring the '
105
                                'attribute.'
106
                                .format(attr, get_class_name(cls), err.message))
107
                break
108
        if not (strip_nulls and dumped_elem is None):
109
            if key_transformer:
110
                attr_name = key_transformer(attr_name)
111
            result[attr_name] = dumped_elem
112
    cls_name = get_class_name(cls, fully_qualified=True, fork_inst=fork_inst)
113
    if not kwargs.get('_store_cls'):
114
        result = _get_dict_with_meta(result, cls_name, verbose, fork_inst)
115
    return result
116
117
118
def _check_slots(cls: type, kwargs):
119
    # Check for __slots__ or __dataclass_fields__.
120
    if (cls and not hasattr(cls, '__slots__')
121
            and not hasattr(cls, '__annotations__')
122
            and not hasattr(cls, '__dataclass_fields__')):
123
        raise SerializationError('Invalid type: "{}". Only dataclasses or '
124
                                 'types that have a __slots__ defined are '
125
                                 'allowed when providing "cls".'
126
                                 .format(get_class_name(cls, fork_inst=kwargs['fork_inst'], fully_qualified=True)))
127
128
129
def _normalize_strip_attr(strip_attr) -> tuple:
130
    # Make sure that strip_attr is always a tuple.
131
    strip_attr = strip_attr or tuple()
132
    if (not isinstance(strip_attr, MutableSequence)
133
            and not isinstance(strip_attr, tuple)):
134
        strip_attr = (strip_attr,)
135
    return strip_attr
136
137
138
@cached
139
def _get_attributes_from_class(
140
        cls: type,
141
        strip_privates: bool,
142
        strip_properties: bool,
143
        strip_class_variables: bool,
144
        strip_attr: tuple) -> Dict[str, Optional[type]]:
145
    # Get the attributes that are known in the class.
146
    attributes_and_types = _get_attributes_and_types(cls)
147
    return _filter_attributes(cls, attributes_and_types, strip_privates,
148
                              strip_properties, strip_class_variables,
149
                              strip_attr)
150
151
152
def _get_attributes_from_object(
153
        obj: object,
154
        strip_privates: bool,
155
        strip_properties: bool,
156
        strip_class_variables: bool,
157
        strip_attr: tuple) -> Dict[str, Optional[type]]:
158
    # Get the attributes that are known in the object.
159
    cls = obj.__class__
160
    attributes_and_types = _get_attributes_and_types(cls)
161
    attributes = {attr: attributes_and_types.get(attr, None)
162
                  for attr in dir(obj)}
163
    return _filter_attributes(cls, attributes, strip_privates,
164
                              strip_properties, strip_class_variables,
165
                              strip_attr)
166
167
168
@cached
169
def _get_attributes_and_types(cls) -> Dict[str, Optional[type]]:
170
    if '__slots__' in cls.__dict__:
171
        attributes = {attr: None for attr in cls.__slots__}
172
    elif hasattr(cls, '__annotations__'):
173
        attributes = cls.__annotations__
174
    else:
175
        attributes = {}
176
    return attributes
177
178
179
def _filter_attributes(
180
        cls: type,
181
        attributes: Dict[str, Optional[type]],
182
        strip_privates: bool,
183
        strip_properties: bool,
184
        strip_class_variables: bool,
185
        strip_attr: tuple) -> Dict[str, Optional[type]]:
186
    # Filter the given attributes with the given preferences.
187
    strip_attr = strip_attr + _ABC_ATTRS
188
    excluded_elems = dir(JsonSerializable)
189
    props, other_cls_vars = _get_class_props(cls)
190
191
    return {attr: type_ for attr, type_ in attributes.items()
192
            if not attr.startswith('__')
193
            and not (strip_privates and attr.startswith('_'))
194
            and not (strip_properties and attr in props)
195
            and not (strip_class_variables and attr in other_cls_vars)
196
            and attr not in strip_attr
197
            and attr != 'json'
198
            and not isfunction(getattr(cls, attr, None))
199
            and attr not in excluded_elems}
200
201
202
def _get_class_props(cls: type) -> Tuple[list, list]:
203
    props = []
204
    other_cls_vars = []
205
    for n, v in _get_complete_class_dict(cls).items():
206
        list_to_append = props if isinstance(v, property) else other_cls_vars
207
        list_to_append.append(n)
208
    return props, other_cls_vars
209
210
211
def _get_complete_class_dict(cls: type) -> dict:
212
    cls_dict = {}
213
    # Loop reversed so values of sub-classes override those of super-classes.
214
    for cls_or_elder in reversed(cls.mro()):
215
        cls_dict.update(cls_or_elder.__dict__)
216
    return cls_dict
217
218
219
def _get_dict_with_meta(
220
        obj: dict,
221
        cls_name: str,
222
        verbose: Verbosity,
223
        fork_inst: type) -> dict:
224
    # This function will add a -meta section to the given obj (provided that
225
    # the given obj has -cls attributes for all children).
226
    if verbose is Verbosity.WITH_NOTHING:
227
        return obj
228
229
    obj[META_ATTR] = {}
230
    if Verbosity.WITH_CLASS_INFO in verbose:
231
        collection_of_types = {}
232
        _fill_collection_of_types(obj, cls_name, '/', collection_of_types)
233
        collection_of_types['/'] = cls_name
234
        obj[META_ATTR]['classes'] = collection_of_types
235
    if Verbosity.WITH_DUMP_TIME in verbose:
236
        dump_time = to_str(datetime.now(tz=timezone.utc), True, fork_inst)
237
        obj[META_ATTR]['dump_time'] = dump_time
238
    return obj
239
240
241
def _fill_collection_of_types(
242
        obj_: dict,
243
        cls_name_: Optional[str],
244
        prefix: str,
245
        collection_of_types_: dict) -> str:
246
    # This function loops through obj_ to fill collection_of_types_ with the
247
    # class names. All of the -cls attributes are removed in the process.
248
    cls_name_ = _get_class_name_and_strip_cls(cls_name_, obj_)
249
    for attr in obj_:
250
        if attr != META_ATTR and isinstance(obj_[attr], dict):
251
            attr_class = _fill_collection_of_types(obj_[attr],
252
                                                   None,
253
                                                   prefix + attr + '/',
254
                                                   collection_of_types_)
255
            collection_of_types_[prefix + attr] = attr_class
256
    return cls_name_
257
258
259
def _get_class_name_and_strip_cls(cls_name: Optional[str], obj: dict) -> str:
260
    result = cls_name
261
    if not cls_name and '-cls' in obj:
262
        result = obj['-cls']
263
    if '-cls' in obj:
264
        del obj['-cls']
265
    return result
266
267
268
def _store_cls_info(result: object, original_obj: dict, kwargs):
269
    if kwargs.get('_store_cls', None) and isinstance(result, dict):
270
        cls = get_type(original_obj)
271
        if cls.__module__ == 'typing':
272
            cls_name = repr(cls)
273
        else:
274
            cls_name = get_class_name(cls, fully_qualified=True,
275
                                      fork_inst=kwargs['fork_inst'])
276
        result['-cls'] = cls_name
277
278
279
_ABC_ATTRS = ('_abc_registry', '_abc_cache', '_abc_negative_cache',
280
              '_abc_negative_cache_version', '_abc_impl')
281