Completed
Push — develop ( b4a21c...b1df34 )
by Jace
02:37
created

Dictionary   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 73
Duplicated Lines 0 %

Test Coverage

Coverage 100%
Metric Value
wmc 16
dl 0
loc 73
ccs 50
cts 50
cp 1
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
F update_value() 0 53 12
A __new__() 0 5 2
A to_data() 0 10 2
1
"""Converter classes for builtin container types."""
2
3 1
from .. import common
4 1
from ..bases import Container
5 1
from . import standard
6
7 1
log = common.logger(__name__)
8
9
10 1
class Dictionary(Container, dict):
11
    """Base class for a dictionary of attribute types."""
12
13 1
    def __new__(cls, *args, **kwargs):
14 1
        if cls is Dictionary:
15 1
            msg = "Dictionary class must be subclassed to use"
16 1
            raise NotImplementedError(msg)
17 1
        return super().__new__(cls, *args, **kwargs)
18
19 1
    @classmethod
20
    def to_data(cls, value):
21 1
        value2 = cls.create_default()
22 1
        value2.update_value(value, strict=True)
23
24 1
        data = common.attrs[cls].__class__()
25 1
        for name, converter in common.attrs[cls].items():
26 1
            data[name] = converter.to_data(value2.get(name, None))
27
28 1
        return data
29
30 1
    def update_value(self, data, strict=False):
31 1
        cls = self.__class__
32 1
        value = cls.create_default()
33
34
        # Convert object attributes to a dictionary
35 1
        attrs = common.attrs[cls].copy()
36 1
        if isinstance(data, cls):
37 1
            dictionary = {}
38 1
            for k, v in data.items():
39 1
                if k in attrs:
40 1
                    dictionary[k] = v
41 1
            for k, v in data.__dict__.items():
42 1
                if k in attrs:
43 1
                    dictionary[k] = v
44
        else:
45 1
            dictionary = to_dict(data)
46
47
        # Map object attributes to types
48 1
        for name, data2 in dictionary.items():
49
50 1
            try:
51 1
                converter = attrs.pop(name)
52 1
            except KeyError:
53 1
                if strict:
54 1
                    msg = "Ignored unknown nested file attribute: %s = %r"
55 1
                    log.warning(msg, name, data2)
56 1
                    continue
57
                else:
58 1
                    converter = standard.match(name, data2, nested=True)
59 1
                    common.attrs[cls][name] = converter
60
61 1
            try:
62 1
                attr = self[name]
63 1
            except KeyError:
64 1
                attr = converter.create_default()
65
66 1
            if all((isinstance(attr, converter),
67
                    issubclass(converter, Container))):
68 1
                attr.update_value(data2, strict=strict)
69
            else:
70 1
                attr = converter.to_value(data2)
71
72 1
            value[name] = attr
73
74
        # Create default values for unmapped types
75 1
        for name, converter in attrs.items():
76 1
            value[name] = converter.create_default()
77 1
            msg = "Default value for missing nested object attribute: %s = %r"
78 1
            log.info(msg, name, value[name])
79
80
        # Apply the new value
81 1
        self.clear()
82 1
        self.update(value)
83
84
85 1
class List(Container, list):
86
    """Base class for a homogeneous list of attribute types."""
87
88 1
    def __new__(cls, *args, **kwargs):
89 1
        if cls is List:
90 1
            raise NotImplementedError("List class must be subclassed to use")
91 1
        if not cls.item_type:
92 1
            raise NotImplementedError("List subclass must specify item type")
93 1
        return super().__new__(cls, *args, **kwargs)
94
95 1
    @classmethod
96
    def of_type(cls, sub_class):
97 1
        name = sub_class.__name__ + cls.__name__
98 1
        new_class = type(name, (cls,), {})
99 1
        common.attrs[new_class][common.ALL] = sub_class
100 1
        return new_class
101
102 1
    @common.classproperty
103
    def item_type(cls):  # pylint: disable=no-self-argument
104
        """Get the converter class for all items."""
105 1
        return common.attrs[cls].get(common.ALL)
106
107 1
    @classmethod
108
    def to_data(cls, value):
109 1
        value2 = cls.create_default()
110 1
        value2.update_value(value, strict=True)
111
112 1
        data = []
113
114 1
        if value2:
115 1
            for item in value2:
116 1
                data.append(cls.item_type.to_data(item))  # pylint: disable=no-member
117
118 1
        return data
119
120 1
    def update_value(self, data, strict=False):
121 1
        cls = self.__class__
122 1
        value = cls.create_default()
123
124
        # Get the converter for all items
125 1
        converter = cls.item_type
126
127
        # Convert the loaded data
128 1
        for item in to_list(data):
129
130 1
            try:
131 1
                attr = self[len(value)]
132 1
            except IndexError:
133 1
                attr = converter.create_default()  # pylint: disable=no-member
134
135 1
            if all((isinstance(attr, converter),
136
                    issubclass(converter, Container))):
137 1
                attr.update_value(item, strict=strict)
138
            else:
139 1
                attr = converter.to_value(item)  # pylint: disable=no-member
140
141 1
            value.append(attr)
142
143
        # Apply the new value
144 1
        self[:] = value[:]
145
146
147 1
def to_dict(obj):
148
    """Convert a dictionary-like object to a dictionary.
149
150
    >>> to_dict({'key': 42})
151
    {'key': 42}
152
153
    >>> to_dict("key=42")
154
    {'key': '42'}
155
156
    >>> to_dict("key")
157
    {'key': None}
158
159
    >>> to_dict(None)
160
    {}
161
162
    """
163 1
    if isinstance(obj, dict):
164 1
        return obj
165 1
    elif isinstance(obj, str):
166 1
        text = obj.strip()
167 1
        parts = text.split('=')
168 1
        if len(parts) == 2:
169 1
            return {parts[0]: parts[1]}
170
        else:
171 1
            return {text: None}
172
    else:
173 1
        try:
174 1
            return obj.__dict__
175 1
        except AttributeError:
176 1
            return {}
177
178
179 1
def to_list(obj):
180
    """Convert a list-like object to a list.
181
182
    >>> to_list([1, 2, 3])
183
    [1, 2, 3]
184
185
    >>> to_list("a,b,c")
186
    ['a', 'b', 'c']
187
188
    >>> to_list("item")
189
    ['item']
190
191
    >>> to_list(None)
192
    []
193
194
    """
195 1
    if isinstance(obj, list):
196 1
        return obj
197 1
    elif isinstance(obj, str):
198 1
        text = obj.strip()
199 1
        if ',' in text and ' ' not in text:
200 1
            return text.split(',')
201
        else:
202 1
            return text.split()
203 1
    elif obj is not None:
204 1
        return [obj]
205
    else:
206
        return []
207