Passed
Push — master ( 68db0c...36a34f )
by sosei
01:23
created

multivalued_dict_package.multivalued_dict_module   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 521
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 52
eloc 117
dl 0
loc 521
rs 7.44
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A multivalued_dict.keys() 0 13 1
A multivalued_dict.pop() 0 21 2
B multivalued_dict.__delkv__() 0 33 7
A multivalued_dict.fromkeys() 0 12 1
A multivalued_dict.__matchkv__() 0 11 1
A multivalued_dict.__lenvalue__() 0 12 2
A multivalued_dict.__init__() 0 35 5
A multivalued_dict.items() 0 13 1
A multivalued_dict.__setitem__() 0 17 1
A multivalued_dict.values() 0 13 1
A multivalued_dict.popitem() 0 11 1
A multivalued_dict.__len__() 0 9 1
A multivalued_dict.__repr__() 0 5 1
A multivalued_dict.__eq__() 0 13 1
A multivalued_dict.count() 0 7 1
A multivalued_dict.__getitem__() 0 16 2
A multivalued_dict.__delitem__() 0 10 1
A multivalued_dict.__iter__() 0 12 1
A multivalued_dict.reverse() 0 10 1
A multivalued_dict.clear() 0 12 1
A multivalued_dict.setdefault() 0 15 1
D multivalued_dict.update() 0 71 12
A multivalued_dict.get() 0 11 1
A multivalued_dict.copy() 0 17 1
A multivalued_dict.__is_multivalued_dict__() 0 8 3
A multivalued_dict.__contains__() 0 13 1

How to fix   Complexity   

Complexity

Complex classes like multivalued_dict_package.multivalued_dict_module often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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