Completed
Push — master ( 42856b...8b294e )
by Satoru
01:35
created

m9dicts.UpdateWithReplaceDict.update()   B

Complexity

Conditions 7

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 20
rs 7.3333
cc 7
1
#
2
# Copyright (C) 2011 - 2015 Red Hat, Inc.
3
# Copyright (C) 2011 - 2016 Satoru SATOH <ssato redhat.com>
4
# License: MIT
5
#
6
# .. note:: suppress warning about import error of ordereddict only for py-2.6.
7
# pylint: disable=import-error
8
"""Some dict-like classes support merge operations.
9
"""
10
from __future__ import absolute_import
11
12
import m9dicts.globals as MG
13
import m9dicts.utils
14
15
from m9dicts.compat import OrderedDict
16
17
18
def _are_list_like(*objs):
19
    """
20
    >>> _are_list_like([], (), [x for x in range(10)], (x for x in range(4)))
21
    True
22
    >>> _are_list_like([], {})
23
    False
24
    >>> _are_list_like([], "aaa")
25
    False
26
    """
27
    return all(m9dicts.utils.is_list_like(obj) for obj in objs)
28
29
30
class UpdateWithReplaceDict(dict):
31
    """
32
    Replace value of self with other's if both have same keys on update.
33
    Otherwise, just keep the value of self.
34
35
    >>> od0 = OrderedDict((("a", 1), ("b", [1, 3]), ("c", "abc"), ("f", None)))
36
    >>> md0 = UpdateWithReplaceDict(od0)
37
    >>> upd = UpdateWithReplaceDict(a=2, b=[0, 1], c=dict(d="d", e=1), d="d")
38
    >>> (ref, md1, md2, md3) = [md0.copy() for _ in range(4)]
39
    >>> md0.update(upd)
40
    >>> md1.update(dict(upd))
41
    >>> md2.update(list(upd.items()))
42
    >>> md3.update(**upd)
43
    >>> for md in (md0, md1, md2, md3):
44
    ...     assert all(md[k] == upd[k] for k in upd.keys())
45
    ...     assert all(md[k] == ref[k] for k in ref.keys() if k not in upd)
46
    >>> md5 = UpdateWithReplaceDict(1)  # doctest: +ELLIPSIS
47
    Traceback (most recent call last):
48
    TypeError: ...
49
    """
50
    def _update(self, other, key, *args):
51
        """
52
        :param other: a dict[-like] object or a list of (key, value) tuples
53
        :param key: object key
54
        :param args: [] or (value, ...)
55
        """
56
        self[key] = args[0] if args else other[key]
57
58
    def update(self, *others, **another):
59
        """
60
        :param others: a list of dict[-like] objects or (key, value) tuples
61
        :param another: optional keyword arguments to update self more
62
63
        .. seealso:: Document of dict.update
64
        """
65
        for other in others:
66
            if hasattr(other, "keys"):
67
                for key in other.keys():
68
                    self._update(other, key)
69
            else:
70
                try:
71
                    for key, val in other:
72
                        self._update(other, key, val)
73
                except (ValueError, TypeError) as exc:
74
                    raise type(exc)(str(exc) + " other=%r" % other)
75
76
        for key in another.keys():
77
            self._update(another, key)
78
79
80
class UpdateWoReplaceDict(UpdateWithReplaceDict):
81
    """
82
    Never update (replace) the value of self with other's, that is, only the
83
    values self does not have its key will be added on update.
84
85
    >>> od0 = OrderedDict((("a", 1), ("b", [1, 3]), ("c", "abc"),
86
    ...                    ("f", None)))
87
    >>> md0 = UpdateWoReplaceDict(od0)
88
    >>> ref = md0.copy()
89
    >>> upd = UpdateWoReplaceDict(a=2, b=[0, 1], c="xyz", d=None)
90
    >>> md0.update(upd)
91
    >>> all(md0[k] == upd[k] for k in upd.keys() if k not in ref)
92
    True
93
    >>> all(md0[k] == ref[k] for k in ref.keys())
94
    True
95
    """
96
    def _update(self, other, key, *args):
97
        """
98
        :param other: a dict[-like] object or a list of (key, value) tuples
99
        :param key: object key
100
        :param args: [] or (value, ...)
101
        """
102
        if key not in self:
103
            self[key] = args[0] if args else other[key]
104
105
106
class UpdateWithMergeDict(UpdateWithReplaceDict):
107
    """
108
    Merge the value of self with other's recursively. Behavior of merge will be
109
    vary depends on types of original and new values.
110
111
    - dict-like vs. dict-like -> merge recursively
112
    - list vs. list -> vary depends on `merge_lists`. see its description.
113
    - other objects vs. any -> vary depends on `keep`. see its description.
114
115
    class variables:
116
117
        - merge_lists: Merge not only dicts but also lists. For example,
118
119
            [1, 2, 3], [3, 4] ==> [1, 2, 3, 4]
120
            [1, 2, 2], [2, 4] ==> [1, 2, 2, 4]
121
122
        - keep: Keep original value if type of original value is not a dict nor
123
          list. It will be simply replaced with new value by default.
124
125
    >>> od0 = OrderedDict((("c", 2), ("d", 3)))
126
    >>> od1 = OrderedDict((("c", 4), ("d", 5), ("g", None)))
127
    >>> md0 = UpdateWithMergeDict((("a", 1), ("b", UpdateWithMergeDict(od0)),
128
    ...                            ("e", [1, 2, 2]), ("f", None)))
129
    >>> upd = dict(a=2, b=od1, e=[2, 3, 4])
130
    >>> ref = md0.copy()
131
    >>> md0.update(upd)
132
    >>> all(md0[k] == upd[k] for k in ("a", "e"))  # vary depends on 'keep'.
133
    True
134
    >>> all(md0["b"][k] == ref["b"][k] for k in ref["b"].keys())
135
    True
136
    >>> all(md0[k] == ref[k] for k in ref.keys() if k not in upd)
137
    True
138
    >>> class Foo(UpdateWithMergeDict):
139
    ...     keep = True
140
    >>> md1 = Foo(a=1, b=2)
141
    >>> md1.update(a=2, c=3)
142
    >>> md1["a"]
143
    1
144
    >>> md1["c"]
145
    3
146
    """
147
    merge_lists = False
148
    keep = False
149
150
    def _update(self, other, key, *args):
151
        """
152
        :param other: a dict[-like] object or a list of (key, value) tuples
153
        :param key: object key
154
        :param args: [] or (value, ...)
155
        """
156
        val = args[0] if args else other[key]
157
        if key in self:
158
            val0 = self[key]  # Original value
159
            if m9dicts.utils.is_dict_like(val0):  # It needs recursive updates.
160
                self[key].update(val)
161
            elif self.merge_lists and _are_list_like(val, val0):
162
                self[key] += [x for x in val if x not in val0]
163
            elif not self.keep:
164
                self[key] = val  # Overwrite it.
165
        else:
166
            self[key] = val
167
168
169
class UpdateWithMergeListsDict(UpdateWithMergeDict):
170
    """
171
    Similar to UpdateWithMergeDict but merge lists by default.
172
173
    >>> md0 = UpdateWithMergeListsDict(a=[1, 2, 3])
174
    >>> md0.update(a=[4, 4, 5])
175
    >>> md0["a"]
176
    [1, 2, 3, 4, 4, 5]
177
    """
178
    merge_lists = True
179
180
181
class UpdateWithReplaceOrderedDict(UpdateWithReplaceDict, OrderedDict):
182
    """
183
    Similar to UpdateWithReplaceDict but keep keys' order like OrderedDict.
184
185
    >>> od0 = OrderedDict((("a", 1), ("b", [1, 3]), ("c", "abc"), ("f", None)))
186
    >>> od1 = OrderedDict((("a", 2), ("b", [0, 1]),
187
    ...                    ("c", OrderedDict((("d", "d"), ("e", 1)))),
188
    ...                    ("d", "d")))
189
    >>> md0 = UpdateWithReplaceOrderedDict(od0)
190
    >>> md1 = UpdateWithReplaceOrderedDict(od0.items())
191
    >>> upd = UpdateWithReplaceOrderedDict(od1)
192
    >>> ref = md0.copy()
193
    >>> for md in (md0, md1):
194
    ...     md.update(upd)
195
    ...     assert all(md0[k] == upd[k] for k in upd.keys())
196
    ...     assert all(md0[k] == ref[k] for k in ref.keys() if k not in upd)
197
    ...     assert list(md0.keys()) == ['a', 'b', 'c', 'f', 'd']
198
    >>>
199
    """
200
    pass
201
202
203
class UpdateWoReplaceOrderedDict(UpdateWoReplaceDict, OrderedDict):
204
    """
205
    Similar to UpdateWoReplaceDict but keep keys' order like OrderedDict.
206
207
    >>> od0 = OrderedDict((("a", 1), ("b", [1, 3]), ("c", "abc"),
208
    ...                    ("f", None)))
209
    >>> md0 = UpdateWoReplaceOrderedDict(od0)
210
    >>> upd = UpdateWoReplaceOrderedDict((("a", 2), ("b", [0, 1]),
211
    ...                                   ("c", "xyz"), ("d", None)))
212
    >>> ref = md0.copy()
213
    >>> md0.update(upd)
214
    >>> all(md0[k] == upd[k] for k in upd.keys() if k not in ref)
215
    True
216
    >>> all(md0[k] == ref[k] for k in ref.keys())
217
    True
218
    >>> list(md0.keys())
219
    ['a', 'b', 'c', 'f', 'd']
220
    """
221
    pass
222
223
224
class UpdateWithMergeOrderedDict(UpdateWithMergeDict, OrderedDict):
225
    """
226
    Similar to UpdateWithMergeDict but keep keys' order like OrderedDict.
227
228
    >>> od0 = OrderedDict((("c", 2), ("d", 3)))
229
    >>> od1 = OrderedDict((("c", 4), ("d", 5), ("g", None)))
230
    >>> md0 = UpdateWithMergeOrderedDict(
231
    ...          OrderedDict((("a", 1),
232
    ...                       ("b", UpdateWithMergeOrderedDict(od0)),
233
    ...                       ("e", [1, 2, 2]), ("f", None))))
234
    >>> ref = md0.copy()
235
    >>> upd = dict(a=2, b=UpdateWithMergeOrderedDict(od1), e=[2, 3, 4])
236
    >>> md0.update(upd)
237
    >>> all(md0[k] == upd[k] for k in ("a", "e"))
238
    True
239
    >>> all(md0[k] == ref[k] for k in ref.keys() if k not in upd)
240
    True
241
    >>> all(md0["b"][k] == ref["b"][k] for k in ref["b"].keys())
242
    True
243
    >>> list(md0.keys())
244
    ['a', 'b', 'e', 'f']
245
    >>> list(md0["b"].keys())
246
    ['c', 'd', 'g']
247
    """
248
    pass
249
250
251
class UpdateWithMergeListsOrderedDict(UpdateWithMergeListsDict, OrderedDict):
252
    """
253
    Similar to UpdateWithMergeListsDict but keep keys' order like OrderedDict.
254
255
    >>> md0 = UpdateWithMergeListsOrderedDict((("a", [1, 2, 3]), ('b', 0)))
256
    >>> md0.update(a=[4, 4, 5])
257
    >>> md0["a"]
258
    [1, 2, 3, 4, 4, 5]
259
    >>> list(md0.keys())
260
    ['a', 'b']
261
    """
262
    pass
263
264
265
# Mapppings: (merge_strategy, ordered?) : m9dict class
266
_MDICTS_MAP = {(MG.MS_REPLACE, True): UpdateWithReplaceOrderedDict,
267
               (MG.MS_REPLACE, False): UpdateWithReplaceDict,
268
               (MG.MS_NO_REPLACE, True): UpdateWoReplaceOrderedDict,
269
               (MG.MS_NO_REPLACE, False): UpdateWoReplaceDict,
270
               (MG.MS_DICTS, True): UpdateWithMergeOrderedDict,
271
               (MG.MS_DICTS, False): UpdateWithMergeDict,
272
               (MG.MS_DICTS_AND_LISTS, True): UpdateWithMergeListsOrderedDict,
273
               (MG.MS_DICTS_AND_LISTS, False): UpdateWithMergeListsDict}
274
275
276
def get_mdict_class(merge=MG.MS_DICTS, ordered=False):
277
    """
278
    Select dict-like class based on merge strategy and orderness of keys.
279
280
    :param merge: Specify strategy from MERGE_STRATEGIES of how to merge dicts.
281
    :param ordered:
282
        The class keeps the order of insertion keys such as
283
        UpdateWoReplaceOrderedDict will be chosen if True.
284
285
    :return: The dict class object
286
    """
287
    return _MDICTS_MAP.get((merge, ordered), UpdateWithMergeDict)
288
289
# vim:sw=4:ts=4:et:
290