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

_get_attributes_from_object()   A

Complexity

Conditions 1

Size

Total Lines 14
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 14
rs 9.75
c 0
b 0
f 0
cc 1
nop 5
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