Completed
Push — develop ( 0ce159...3bb0e5 )
by Jace
02:41
created

Dictionary   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 79
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 11
Bugs 0 Features 1
Metric Value
wmc 17
c 11
b 0
f 1
dl 0
loc 79
ccs 54
cts 54
cp 1
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
F update_value() 0 59 13
A __new__() 0 5 2
A to_data() 0 10 2
1
"""Converter classes for builtin container types."""
2
3 1
import logging
4
5 1
from .. import common
6 1
from ..bases import Container
7 1
from . import standard
8
9 1
log = logging.getLogger(__name__)
10
11
12 1
class Dictionary(Container, dict):
13
    """Base class for a dictionary of attribute types."""
14
15 1
    def __new__(cls, *args, **kwargs):
16 1
        if cls is Dictionary:
17 1
            msg = "Dictionary class must be subclassed to use"
18 1
            raise NotImplementedError(msg)
19 1
        return super().__new__(cls, *args, **kwargs)
20
21 1
    @classmethod
22
    def to_data(cls, value):
23 1
        value2 = cls.create_default()
24 1
        value2.update_value(value, auto_track=False)
25
26 1
        data = common.attrs[cls].__class__()
27 1
        for name, converter in common.attrs[cls].items():
28 1
            data[name] = converter.to_data(value2.get(name, None))
29
30 1
        return data
31
32 1
    def update_value(self, data, *, auto_track=True):
33 1
        cls = self.__class__
34 1
        value = cls.create_default()
35
36
        # Convert object attributes to a dictionary
37 1
        attrs = common.attrs[cls].copy()
38 1
        if isinstance(data, cls):
39 1
            dictionary = {}
40 1
            for k, v in data.items():
41 1
                if k in attrs:
42 1
                    dictionary[k] = v
43 1
            for k, v in data.__dict__.items():
44 1
                if k in attrs:
45 1
                    dictionary[k] = v
46
        else:
47 1
            dictionary = to_dict(data)
48
49
        # Map object attributes to types
50 1
        for name, data2 in dictionary.items():
51
52 1
            try:
53 1
                converter = attrs.pop(name)
54 1
            except KeyError:
55 1
                if auto_track:
56 1
                    converter = standard.match(name, data2, nested=True)
57 1
                    common.attrs[cls][name] = converter
58
                else:
59 1
                    msg = "Ignored unknown nested file attribute: %s = %r"
60 1
                    log.warning(msg, name, data2)
61 1
                    continue
62
63 1
            try:
64 1
                attr = self[name]
65 1
            except KeyError:
66 1
                attr = converter.create_default()
67
68 1
            if all((isinstance(attr, converter),
69
                    issubclass(converter, Container))):
70 1
                attr.update_value(data2, auto_track=auto_track)
71
            else:
72 1
                attr = converter.to_value(data2)
73
74 1
            value[name] = attr
75
76
        # Create default values for unmapped types
77 1
        for name, converter in attrs.items():
78 1
            value[name] = converter.create_default()
79 1
            msg = "Default value for missing nested object attribute: %s = %r"
80 1
            log.info(msg, name, value[name])
81
82
        # Execute custom __init__ validations
83 1
        try:
84 1
            cls(**value)
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
85 1
        except TypeError as exception:
86 1
            log.warning("%s: %s", cls.__name__, exception)
87
88
        # Apply the new value
89 1
        self.clear()
90 1
        self.update(value)
91
92
93 1
class List(Container, list):
94
    """Base class for a homogeneous list of attribute types."""
95
96 1
    def __new__(cls, *args, **kwargs):
97 1
        if cls is List:
98 1
            raise NotImplementedError("List class must be subclassed to use")
99 1
        if not cls.item_type:
100 1
            raise NotImplementedError("List subclass must specify item type")
101 1
        return super().__new__(cls, *args, **kwargs)
102
103 1
    @classmethod
104
    def of_type(cls, sub_class):
105 1
        name = sub_class.__name__ + cls.__name__
106 1
        new_class = type(name, (cls,), {})
107 1
        common.attrs[new_class][common.ALL] = sub_class
108 1
        return new_class
109
110 1
    @common.classproperty
111
    def item_type(cls):  # pylint: disable=no-self-argument
112
        """Get the converter class for all items."""
113 1
        return common.attrs[cls].get(common.ALL)
114
115 1
    @classmethod
116
    def to_data(cls, value):
117 1
        value2 = cls.create_default()
118 1
        value2.update_value(value, auto_track=False)
119
120 1
        data = []
121
122 1
        if value2:
123 1
            for item in value2:
124 1
                data.append(cls.item_type.to_data(item))  # pylint: disable=no-member
125
126 1
        return data
127
128 1
    def update_value(self, data, *, auto_track=True):
129 1
        cls = self.__class__
130 1
        value = cls.create_default()
131
132
        # Get the converter for all items
133 1
        converter = cls.item_type
134
135
        # Convert the parsed data
136 1
        for item in to_list(data):
137
138 1
            try:
139 1
                attr = self[len(value)]
140 1
            except IndexError:
141 1
                attr = converter.create_default()  # pylint: disable=no-member
142
143 1
            if all((isinstance(attr, converter),
144
                    issubclass(converter, Container))):
145 1
                attr.update_value(item, auto_track=auto_track)
146
            else:
147 1
                attr = converter.to_value(item)  # pylint: disable=no-member
148
149 1
            value.append(attr)
150
151
        # Apply the new value
152 1
        self[:] = value[:]
153
154
155 1
def to_dict(obj):
156
    """Convert a dictionary-like object to a dictionary.
157
158
    >>> to_dict({'key': 42})
159
    {'key': 42}
160
161
    >>> to_dict("key=42")
162
    {'key': '42'}
163
164
    >>> to_dict("key")
165
    {'key': None}
166
167
    >>> to_dict(None)
168
    {}
169
170
    """
171 1
    if isinstance(obj, dict):
172 1
        return obj
173 1
    elif isinstance(obj, str):
174 1
        text = obj.strip()
175 1
        parts = text.split('=')
176 1
        if len(parts) == 2:
177 1
            return {parts[0]: parts[1]}
178
        else:
179 1
            return {text: None}
180
    else:
181 1
        try:
182 1
            return obj.__dict__
183 1
        except AttributeError:
184 1
            return {}
185
186
187 1
def to_list(obj):
188
    """Convert a list-like object to a list.
189
190
    >>> to_list([1, 2, 3])
191
    [1, 2, 3]
192
193
    >>> to_list("a,b,c")
194
    ['a', 'b', 'c']
195
196
    >>> to_list("item")
197
    ['item']
198
199
    >>> to_list(None)
200
    []
201
202
    """
203 1
    if isinstance(obj, list):
204 1
        return obj
205 1
    elif isinstance(obj, str):
206 1
        text = obj.strip()
207 1
        if ',' in text and ' ' not in text:
208 1
            return text.split(',')
209
        else:
210 1
            return text.split()
211 1
    elif obj is not None:
212 1
        return [obj]
213
    else:
214
        return []
215