UpdateWithReplaceDict.update()   B
last analyzed

Complexity

Conditions 7

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 7
c 3
b 0
f 1
dl 0
loc 20
rs 7.3333
1
#
2
# Copyright (C) 2011 - 2015 Red Hat, Inc.
3
# Copyright (C) 2011 - 2017 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)
45
    ...     assert all(md[k] == ref[k] for k in ref 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:
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:
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 if k not in ref)
92
    True
93
    >>> all(md0[k] == ref[k] for k in ref)
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"])
135
    True
136
    >>> all(md0[k] == ref[k] for k in ref 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 _merge_list(self, key, lst):
151
        """
152
        :param key: self[key] will be updated
153
        :param lst: Other list to merge
154
        """
155
        self[key] += [x for x in lst if x not in self[key]]
156
157
    def _merge_other(self, key, val):
158
        """
159
        :param key: self[key] will be updated
160
        :param val: Other val to merge (update/replace)
161
        """
162
        self[key] = val  # Just overwrite it by default implementation.
163
164
    def _update(self, other, key, *args):
165
        """
166
        :param other: a dict[-like] object or a list of (key, value) tuples
167
        :param key: object key
168
        :param args: [] or (value, ...)
169
        """
170
        val = args[0] if args else other[key]
171
        if key in self:
172
            val0 = self[key]  # Original value
173
            if m9dicts.utils.is_dict_like(val0):  # It needs recursive updates.
174
                self[key].update(val)
175
            elif self.merge_lists and _are_list_like(val, val0):
176
                self._merge_list(key, val)
177
            elif not self.keep:
178
                self._merge_other(key, val)
179
        else:
180
            self[key] = val
181
182
183
class UpdateWithMergeListsDict(UpdateWithMergeDict):
184
    """
185
    Similar to UpdateWithMergeDict but merge lists by default.
186
187
    >>> md0 = UpdateWithMergeListsDict(a=[1, 2, 3])
188
    >>> md0.update(a=[4, 4, 5])
189
    >>> md0["a"]
190
    [1, 2, 3, 4, 4, 5]
191
    """
192
    merge_lists = True
193
194
195
class UpdateWithReplaceOrderedDict(UpdateWithReplaceDict, OrderedDict):
196
    """
197
    Similar to UpdateWithReplaceDict but keep keys' order like OrderedDict.
198
199
    >>> od0 = OrderedDict((("a", 1), ("b", [1, 3]), ("c", "abc"), ("f", None)))
200
    >>> od1 = OrderedDict((("a", 2), ("b", [0, 1]),
201
    ...                    ("c", OrderedDict((("d", "d"), ("e", 1)))),
202
    ...                    ("d", "d")))
203
    >>> md0 = UpdateWithReplaceOrderedDict(od0)
204
    >>> md1 = UpdateWithReplaceOrderedDict(od0.items())
205
    >>> upd = UpdateWithReplaceOrderedDict(od1)
206
    >>> ref = md0.copy()
207
    >>> for md in (md0, md1):
208
    ...     md.update(upd)
209
    ...     assert all(md0[k] == upd[k] for k in upd)
210
    ...     assert all(md0[k] == ref[k] for k in ref if k not in upd)
211
    ...     assert list(md0.keys()) == ['a', 'b', 'c', 'f', 'd']
212
    >>>
213
    """
214
    pass
215
216
217
class UpdateWoReplaceOrderedDict(UpdateWoReplaceDict, OrderedDict):
218
    """
219
    Similar to UpdateWoReplaceDict but keep keys' order like OrderedDict.
220
221
    >>> od0 = OrderedDict((("a", 1), ("b", [1, 3]), ("c", "abc"),
222
    ...                    ("f", None)))
223
    >>> md0 = UpdateWoReplaceOrderedDict(od0)
224
    >>> upd = UpdateWoReplaceOrderedDict((("a", 2), ("b", [0, 1]),
225
    ...                                   ("c", "xyz"), ("d", None)))
226
    >>> ref = md0.copy()
227
    >>> md0.update(upd)
228
    >>> all(md0[k] == upd[k] for k in upd if k not in ref)
229
    True
230
    >>> all(md0[k] == ref[k] for k in ref)
231
    True
232
    >>> list(md0.keys())
233
    ['a', 'b', 'c', 'f', 'd']
234
    """
235
    pass
236
237
238
class UpdateWithMergeOrderedDict(UpdateWithMergeDict, OrderedDict):
239
    """
240
    Similar to UpdateWithMergeDict but keep keys' order like OrderedDict.
241
242
    >>> od0 = OrderedDict((("c", 2), ("d", 3)))
243
    >>> od1 = OrderedDict((("c", 4), ("d", 5), ("g", None)))
244
    >>> md0 = UpdateWithMergeOrderedDict(
245
    ...          OrderedDict((("a", 1),
246
    ...                       ("b", UpdateWithMergeOrderedDict(od0)),
247
    ...                       ("e", [1, 2, 2]), ("f", None))))
248
    >>> ref = md0.copy()
249
    >>> upd = dict(a=2, b=UpdateWithMergeOrderedDict(od1), e=[2, 3, 4])
250
    >>> md0.update(upd)
251
    >>> all(md0[k] == upd[k] for k in ("a", "e"))
252
    True
253
    >>> all(md0[k] == ref[k] for k in ref if k not in upd)
254
    True
255
    >>> all(md0["b"][k] == ref["b"][k] for k in ref["b"])
256
    True
257
    >>> list(md0.keys())
258
    ['a', 'b', 'e', 'f']
259
    >>> list(md0["b"].keys())
260
    ['c', 'd', 'g']
261
    """
262
    pass
263
264
265
class UpdateWithMergeListsOrderedDict(UpdateWithMergeListsDict, OrderedDict):
266
    """
267
    Similar to UpdateWithMergeListsDict but keep keys' order like OrderedDict.
268
269
    >>> md0 = UpdateWithMergeListsOrderedDict((("a", [1, 2, 3]), ('b', 0)))
270
    >>> md0.update(a=[4, 4, 5])
271
    >>> md0["a"]
272
    [1, 2, 3, 4, 4, 5]
273
    >>> list(md0.keys())
274
    ['a', 'b']
275
    """
276
    pass
277
278
279
# Mapppings: (merge_strategy, ordered?) : m9dict class
280
_MDICTS_MAP = {(MG.MS_REPLACE, True): UpdateWithReplaceOrderedDict,
281
               (MG.MS_REPLACE, False): UpdateWithReplaceDict,
282
               (MG.MS_NO_REPLACE, True): UpdateWoReplaceOrderedDict,
283
               (MG.MS_NO_REPLACE, False): UpdateWoReplaceDict,
284
               (MG.MS_DICTS, True): UpdateWithMergeOrderedDict,
285
               (MG.MS_DICTS, False): UpdateWithMergeDict,
286
               (MG.MS_DICTS_AND_LISTS, True): UpdateWithMergeListsOrderedDict,
287
               (MG.MS_DICTS_AND_LISTS, False): UpdateWithMergeListsDict}
288
289
290
def get_mdict_class(merge=MG.MS_DICTS, ordered=False):
291
    """
292
    Select dict-like class based on merge strategy and orderness of keys.
293
294
    :param merge: Specify strategy from MERGE_STRATEGIES of how to merge dicts.
295
    :param ordered:
296
        The class keeps the order of insertion keys such as
297
        UpdateWoReplaceOrderedDict will be chosen if True.
298
299
    :return: The dict class object
300
    """
301
    return _MDICTS_MAP.get((merge, ordered), UpdateWithMergeDict)
302
303
# vim:sw=4:ts=4:et:
304