Passed
Push — master ( b3b610...4b1e9d )
by sosei
59s
created

multivalued_dict.__setitem__()   A

Complexity

Conditions 1

Size

Total Lines 16
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 16
rs 10
c 0
b 0
f 0
cc 1
nop 3
1
'''
2
multivalued_dict - This is a multi-valued dictionary package.
3
Copyright (C) 2019  sosei
4
5
This program is free software: you can redistribute it and/or modify
6
it under the terms of the GNU Affero General Public License as published
7
by the Free Software Foundation, either version 3 of the License, or
8
(at your option) any later version.
9
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
GNU Affero General Public License for more details.
14
15
You should have received a copy of the GNU Affero General Public License
16
along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
'''
18
19
from collections import defaultdict
20
from collections import UserDict
21
from collections.abc import Iterable
22
import inspect
23
24
START_POS = 'S'
25
END_POS = 'E'
26
    
27
class multivalued_dict(UserDict):
28
    '''
29
        multivalued_dict() -> new empty dictionary
30
        multivalued_dict(mapping) -> new dictionary initialized from a mapping object's
31
            (key, value) pairs
32
        multivalued_dict(iterable) -> new dictionary initialized as if via:
33
            d = {}
34
            for k, v in iterable:
35
                d[k].append(v)
36
        multivalued_dict(**kwargs) -> new dictionary initialized with the name=value pairs
37
            in the keyword argument list.  For example:  dict(one=1, two=2)
38
        
39
        >>> mv_d = multivalued_dict()
40
        >>> mv_d
41
        multivalued_dict({})
42
        
43
        >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
44
        >>> mv_d
45
        multivalued_dict({'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3']})
46
        
47
        >>> mv_d = multivalued_dict([['a', 'test-1'], ['b', 'test-2'], ['a', 'test-3']])
48
        >>> mv_d
49
        multivalued_dict({'a': ['test-1', 'test-3'], 'b': ['test-2']})
50
        
51
        >>> mv_d = multivalued_dict(a = 'test-1', b = 'test-2', c = 'test-3')
52
        >>> mv_d
53
        multivalued_dict({'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3']})
54
        
55
        >>> mv_d = multivalued_dict([['a', 'test-1'], ['c', 'test-3']], b = 'test-2')
56
        >>> mv_d
57
        multivalued_dict({'a': ['test-1'], 'c': ['test-3'], 'b': ['test-2']})
58
        
59
        >>> mv_d0 = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
60
        >>> mv_d0
61
        multivalued_dict({'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3']})
62
        >>> mv_d = multivalued_dict(mv_d0)
63
        >>> mv_d
64
        multivalued_dict({'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3']})
65
        
66
        >>> multivalued_dict([['a', 'test-1']], [['b', 'test-2']])
67
        Traceback (most recent call last):
68
        TypeError: multivalued_dict expected at most 1 arguments, got 2
69
    '''
70
    
71
    __marker = object()
72
    
73
    @staticmethod
74
    def __is_self(v_self):
75
        if not multivalued_dict.__is_multivalued_dict__(v_self):
76
            raise TypeError(f"descriptor '{inspect.currentframe().f_back.f_code.co_name}' requires a 'multivalued_dict' object but received a '{v_self.__class__.__name__}'")
77
    
78
    @classmethod
79
    def __is_multivalued_dict__(cls, x):
80
        '''
81
            >>> mv_d = multivalued_dict()
82
            >>> multivalued_dict.__is_multivalued_dict__(mv_d)
83
            True
84
        '''
85
        return (isinstance(x, cls) or ((True if x.default_factory == type([]) else False) if isinstance(x, defaultdict) else False))
86
    
87
    @classmethod
88
    def fromkeys(cls, iterable, value = None):
89
        '''
90
            >>> multivalued_dict.fromkeys(['a', 'b', 'c'])
91
            multivalued_dict({'a': [None], 'b': [None], 'c': [None]})
92
            >>> multivalued_dict.fromkeys(['a', 'b', 'c'], 'test')
93
            multivalued_dict({'a': ['test'], 'b': ['test'], 'c': ['test']})
94
        '''
95
        dict_var = dict.fromkeys(iterable, value)
96
        return cls(dict_var)
97
    
98
    def __init__(self, *args, **kwargs):
99
        '''
100
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
101
            >>> mv_d
102
            multivalued_dict({'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3']})
103
            
104
            >>> mv_d.__init__({'d': 'test-4'})
105
            >>> mv_d
106
            multivalued_dict({'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3'], 'd': ['test-4']})
107
            
108
            >>> multivalued_dict.__init__(mv_d, {'e': 'test-5'})
109
            >>> mv_d
110
            multivalued_dict({'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3'], 'd': ['test-4'], 'e': ['test-5']})
111
            
112
            >>> mv_d.__init__({'a': 'test-6'})
113
            >>> mv_d
114
            multivalued_dict({'a': ['test-1', 'test-6'], 'b': ['test-2'], 'c': ['test-3'], 'd': ['test-4'], 'e': ['test-5']})
115
            
116
            >>> multivalued_dict.__init__('x')
117
            Traceback (most recent call last):
118
            TypeError: descriptor '__init__' requires a 'multivalued_dict' object but received a 'str'
119
        '''
120
        multivalued_dict.__is_self(self)
121
        len_of_args = len(args)
122
        if len_of_args > 1:
123
            raise TypeError(f'multivalued_dict expected at most 1 arguments, got {len_of_args}')
124
        else:
125
            if not hasattr(self, 'data'):
126
                self.data = defaultdict(list)
127
            if len_of_args == 1:
128
                initial_items = args[0]
129
                self.update(initial_items)
130
        if kwargs != dict():
131
            self.update(kwargs)
132
    
133
    def __repr__(self):
134
        multivalued_dict.__is_self(self)
135
        return f'multivalued_dict({dict(self.data)})'
136
    
137
    def __iter__(self):
138
        '''
139
            >>> multivalued_dict(multivalued_dict({'a': 'test-1'}))
140
            multivalued_dict({'a': ['test-1']})
141
            
142
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
143
            >>> multivalued_dict(mv_d.__iter__())
144
            multivalued_dict({'a': [['test-1']], 'b': [['test-2']], 'c': [['test-3']]})
145
        '''
146
        multivalued_dict.__is_self(self)
147
        return iter(self.data.items())
148
    
149
    def __len__(self):
150
        '''
151
            >>> mv_d = multivalued_dict([['a', 'test-1'], ['a', 'test-2'], ['a', 'test-3'], ['b', 'test-4']])
152
            >>> mv_d.__len__()
153
            2
154
        '''
155
        multivalued_dict.__is_self(self)
156
        return self.data.__len__()
157
    
158
    def __lenvalue__(self, key = __marker):
159
        '''
160
            >>> mv_d = multivalued_dict([['a', 1], ['a', 2], ['a', 3], ['b', 1], ['b', 2], ['c', 1]])
161
            >>> mv_d.__lenvalue__()
162
            6
163
            >>> mv_d.__lenvalue__('a')
164
            3
165
        '''
166
        multivalued_dict.__is_self(self)
167
        if key is self.__marker:
168
            return sum(map(len, self.data.values()))
169
        else:
170
            return len(self.data[key])
171
    
172
    def __getitem__(self, key):
173
        '''
174
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
175
            >>> mv_d['a']
176
            ['test-1']
177
            
178
            >>> mv_d['d']
179
            Traceback (most recent call last):
180
            KeyError: 'd'
181
        '''
182
        multivalued_dict.__is_self(self)
183
        if key in self.data:
184
            return self.data[key]
185
        else:
186
            raise KeyError(key)
187
    
188
    def __matchkv__(self, key, value):
189
        '''
190
            >>> mv_d = multivalued_dict([['a', 1], ['a', 2], ['a', 3], ['b', 1], ['b', 2], ['c', 1]])
191
            >>> mv_d.__matchkv__('b', 3)
192
            False
193
            >>> mv_d.__matchkv__('a', 2)
194
            True
195
            >>> mv_d.__matchkv__('d', 1)
196
            False
197
        '''
198
        multivalued_dict.__is_self(self)
199
        return value in self.data[key]
200
    
201
    def __eq__(self, other):
202
        '''
203
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
204
            >>> mv_d
205
            multivalued_dict({'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3']})
206
            >>> mv_d == {'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3']}
207
            True
208
            >>> mv_d == {'a': ['test-1'], 'b': ['test-2'], 'c': ['test-0']}
209
            False
210
        '''
211
        multivalued_dict.__is_self(self)
212
        return self.data.__eq__(other)
213
    
214
    def __contains__(self, key):
215
        '''
216
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
217
            >>> mv_d
218
            multivalued_dict({'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3']})
219
            >>> 'a' in mv_d
220
            True
221
            >>> 'd' in mv_d
222
            False
223
        '''
224
        multivalued_dict.__is_self(self)
225
        return self.data.__contains__(key)
226
    
227
    def __delkv__(self, key, value, allkv = True, direction = START_POS):
228
        '''
229
            >>> mv_d = multivalued_dict([['a', 'x'], ['a', 'y'], ['a', 'z'], ['a', 'y'], ['a', 'z'], ['a', 'y']])
230
            >>> mv_d
231
            multivalued_dict({'a': ['x', 'y', 'z', 'y', 'z', 'y']})
232
            
233
            >>> mv_d.__delkv__('a', 'y', False)
234
            >>> mv_d
235
            multivalued_dict({'a': ['x', 'z', 'y', 'z', 'y']})
236
            
237
            >>> mv_d.__delkv__('a', 'y', False, END_POS)
238
            >>> mv_d
239
            multivalued_dict({'a': ['x', 'z', 'y', 'z']})
240
            
241
            >>> mv_d.__delkv__('a', 'z')
242
            >>> mv_d
243
            multivalued_dict({'a': ['x', 'y']})
244
        '''
245
        multivalued_dict.__is_self(self)
246
        assert allkv in (True, False), '"allkv" can only be True or False'
247
        assert direction in (START_POS, END_POS), '"direction" can only be START_POS or END_POS'
248
        
249
        if allkv:
250
            while value in self.data[key]:
251
                self.data[key].remove(value)
252
        else:
253
            if direction == START_POS:
254
                self.data[key].remove(value)
255
            elif direction == END_POS:
256
                value_len = len(self.data[key])
257
                for i in range(value_len):
258
                    if self.data[key][-1 - i] == value:
259
                        self.data[key].__delitem__(-1 - i)
260
                        break
261
    
262
    def __delitem__(self, key):
263
        '''
264
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
265
            >>> mv_d.__delitem__('b')
266
            >>> mv_d
267
            multivalued_dict({'a': ['test-1'], 'c': ['test-3']})
268
        '''
269
        multivalued_dict.__is_self(self)
270
        self.data.__delitem__(key)
271
    
272
    def __setitem__(self, key, item):
273
        '''
274
            >>> mv_d = multivalued_dict([['a', 'test-1'], ['a', 'test-2'], ['a', 'test-3'], ['b', 'test-4']])
275
            >>> mv_d
276
            multivalued_dict({'a': ['test-1', 'test-2', 'test-3'], 'b': ['test-4']})
277
            
278
            >>> mv_d.__setitem__('c', 'test-5')
279
            >>> mv_d
280
            multivalued_dict({'a': ['test-1', 'test-2', 'test-3'], 'b': ['test-4'], 'c': ['test-5']})
281
            
282
            >>> mv_d.__setitem__('a', 'test-0')
283
            >>> mv_d
284
            multivalued_dict({'a': ['test-0'], 'b': ['test-4'], 'c': ['test-5']})
285
        '''
286
        multivalued_dict.__is_self(self)
287
        self.data.__setitem__(key, [item])
288
    
289
    def get(self, key, default = None):
290
        '''
291
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
292
            >>> mv_d.get('a')
293
            ['test-1']
294
            >>> mv_d.get('d')
295
            [None]
296
        '''
297
        multivalued_dict.__is_self(self)
298
        return self.data.get(key, [default])
299
    
300
    def count(self, key, value):
301
        '''
302
            >>> mv_d = multivalued_dict([['a', 'x'], ['a', 'y'], ['a', 'y'], ['a', 'z'], ['a', 'z'], ['a', 'z']])
303
            >>> mv_d.count('a', 'y')
304
            2
305
        '''
306
        multivalued_dict.__is_self(self)
307
        return self.data[key].count(value)
308
    
309
    def update(self, *args, **kwargs):
310
        '''
311
            >>> mv_d = multivalued_dict()
312
            >>> mv_d
313
            multivalued_dict({})
314
            
315
            >>> mv_d.update({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
316
            >>> mv_d
317
            multivalued_dict({'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3']})
318
            
319
            >>> mv_d.update([['a', 'test-4'], ['a', 'test-5']])
320
            >>> mv_d
321
            multivalued_dict({'a': ['test-1', 'test-4', 'test-5'], 'b': ['test-2'], 'c': ['test-3']})
322
            
323
            >>> mv_d.update(c = 'test-3')
324
            >>> mv_d
325
            multivalued_dict({'a': ['test-1', 'test-4', 'test-5'], 'b': ['test-2'], 'c': ['test-3', 'test-3']})
326
            
327
            >>> mv_d.update([['b', 'test-6'], ['c', 'test-7']], a = 'test-8')
328
            >>> mv_d
329
            multivalued_dict({'a': ['test-1', 'test-4', 'test-5', 'test-8'], 'b': ['test-2', 'test-6'], 'c': ['test-3', 'test-3', 'test-7']})
330
            
331
            >>> mv_d.update(multivalued_dict({'d': 'test-9', 'e': 'test-10'}))
332
            >>> mv_d
333
            multivalued_dict({'a': ['test-1', 'test-4', 'test-5', 'test-8'], 'b': ['test-2', 'test-6'], 'c': ['test-3', 'test-3', 'test-7'], 'd': ['test-9'], 'e': ['test-10']})
334
335
            >>> mv_d.update([['a', 'test-1']], [['b', 'test-2']])
336
            Traceback (most recent call last):
337
            TypeError: multivalued_dict expected at most 1 arguments, got 2
338
339
            >>> mv_d.update(1)
340
            Traceback (most recent call last):
341
            TypeError: 'int' object is not iterable
342
            
343
            >>> mv_d.update([1])
344
            Traceback (most recent call last):
345
            TypeError: cannot convert dictionary update sequence element #0 to a sequence
346
            
347
            >>> mv_d.update([['1', '2', '3']])
348
            Traceback (most recent call last):
349
            ValueError: dictionary update sequence element #0 has length 3; 2 is required
350
            
351
            >>> multivalued_dict.update('x')
352
            Traceback (most recent call last):
353
            TypeError: descriptor 'update' requires a 'multivalued_dict' object but received a 'str'
354
        '''
355
        multivalued_dict.__is_self(self)
356
        len_of_args = len(args)
357
        if len_of_args > 1:
358
            raise TypeError(f'multivalued_dict expected at most 1 arguments, got {len_of_args}')
359
        if len_of_args == 1:
360
            update_items = args[0]
361
            if not isinstance(update_items, Iterable):
362
                raise TypeError(f"'{update_items.__class__.__name__}' object is not iterable")
363
            if multivalued_dict.__is_multivalued_dict__(update_items):
364
                for _key, _value in update_items.items():
365
                    self.data[_key].extend(_value)
366
            elif isinstance(update_items, dict):
367
                for _key, _value in update_items.items():
368
                    self.data[_key].append(_value)
369
            else:
370
                i = 0
371
                for item in update_items:
372
                    if not isinstance(item, Iterable):
373
                        raise TypeError(f'cannot convert dictionary update sequence element #{i} to a sequence')
374
                    if len(item) != 2:
375
                        raise ValueError(f'dictionary update sequence element #{i} has length {len(item)}; 2 is required')
376
                    _key, _value = item
377
                    self.data[_key].append(_value)
378
                    i += 1
379
        if kwargs != dict():
380
            self.update(kwargs)
381
    
382
    def setdefault(self, key, default = None):
383
        '''
384
            >>> mv_d = multivalued_dict({'a': 'test-1', 'c': 'test-3'})
385
            >>> mv_d.setdefault('a')
386
            ['test-1']
387
            >>> mv_d.setdefault('b')
388
            [None]
389
            >>> mv_d
390
            multivalued_dict({'a': ['test-1'], 'c': ['test-3'], 'b': [None]})
391
            >>> mv_d.setdefault('d', 'test=4')
392
            ['test=4']
393
            >>> mv_d
394
            multivalued_dict({'a': ['test-1'], 'c': ['test-3'], 'b': [None], 'd': ['test=4']})
395
        '''
396
        multivalued_dict.__is_self(self)
397
        return self.data.setdefault(key, [default])
398
    
399
    def pop(self, key, default=__marker):
400
        '''
401
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
402
            >>> mv_d.pop('b')
403
            ['test-2']
404
            >>> mv_d
405
            multivalued_dict({'a': ['test-1'], 'c': ['test-3']})
406
            
407
            >>> mv_d.pop('d')
408
            Traceback (most recent call last):
409
            KeyError: 'd'
410
            
411
            >>> mv_d.pop('d', 'test-0')
412
            ['test-0']
413
            >>> mv_d
414
            multivalued_dict({'a': ['test-1'], 'c': ['test-3']})
415
        '''
416
        multivalued_dict.__is_self(self)
417
        if default is self.__marker:
418
            return self.data.pop(key)
419
        else:
420
            return self.data.pop(key, [default])
421
    
422
    def popitem(self):
423
        '''
424
            D.popitem() -> (k, v), remove and return some (key, value) pair as a 2-tuple; but raise KeyError if D is empty.
425
            
426
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
427
            >>> mv_d
428
            multivalued_dict({'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3']})
429
            >>> mv_d.popitem()
430
            ('c', ['test-3'])
431
        '''
432
        multivalued_dict.__is_self(self)
433
        return self.data.popitem()
434
    
435
    def copy(self):
436
        '''
437
            D.copy() -> a shallow copy of D
438
            
439
            >>> mv_d_a = multivalued_dict([['a', 1], ['a', 2], ['a', 3]])
440
            >>> mv_d_b = mv_d_a.copy()
441
            >>> mv_d_a
442
            multivalued_dict({'a': [1, 2, 3]})
443
            >>> mv_d_b
444
            multivalued_dict({'a': [1, 2, 3]})
445
            >>> mv_d_a['a'][1] = 99
446
            >>> mv_d_a
447
            multivalued_dict({'a': [1, 99, 3]})
448
            >>> mv_d_b
449
            multivalued_dict({'a': [1, 2, 3]})
450
        '''
451
        multivalued_dict.__is_self(self)
452
        return multivalued_dict(self.data)
453
    
454
    def items(self):
455
        '''
456
            D.items() -> a set-like object providing a view on D's items
457
            
458
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
459
            >>> for k, v in mv_d.items():
460
            ...     print(f'key = {k}, value = {v}')
461
            ...
462
            key = a, value = ['test-1']
463
            key = b, value = ['test-2']
464
            key = c, value = ['test-3']
465
        '''
466
        multivalued_dict.__is_self(self)
467
        return self.data.items()
468
    
469
    def keys(self):
470
        '''
471
            D.keys() -> a set-like object providing a view on D's keys
472
            
473
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
474
            >>> for k in mv_d.keys():
475
            ...     print(f'key = {k}')
476
            ...
477
            key = a
478
            key = b
479
            key = c
480
        '''
481
        multivalued_dict.__is_self(self)
482
        return self.data.keys()
483
    
484
    def values(self):
485
        '''
486
            D.values() -> an object providing a view on D's values
487
            
488
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
489
            >>> for v in mv_d.values():
490
            ...     print(f'value = {v}')
491
            ...
492
            value = ['test-1']
493
            value = ['test-2']
494
            value = ['test-3']
495
        '''
496
        multivalued_dict.__is_self(self)
497
        return self.data.values()
498
    
499
    def clear(self):
500
        '''
501
            D.clear() -> None.  Remove all items from D.
502
            
503
            >>> mv_d = multivalued_dict({'a': 'test-1', 'b': 'test-2', 'c': 'test-3'})
504
            >>> mv_d
505
            multivalued_dict({'a': ['test-1'], 'b': ['test-2'], 'c': ['test-3']})
506
            >>> mv_d.clear()
507
            >>> mv_d
508
            multivalued_dict({})
509
        '''
510
        multivalued_dict.__is_self(self)
511
        self.data.clear()
512
513
    def reverse(self, key):
514
        '''
515
            >>> mv_d = multivalued_dict([['a', 1], ['a', 2], ['a', 3]])
516
            >>> mv_d
517
            multivalued_dict({'a': [1, 2, 3]})
518
            >>> mv_d.reverse('a')
519
            >>> mv_d
520
            multivalued_dict({'a': [3, 2, 1]})
521
        '''
522
        multivalued_dict.__is_self(self)
523
        self.data[key].reverse()
524