1'''
2Created on 26 May 2013
3
4@author: lukasz.forynski
5
6@brief: Implementation of the multi-key dictionary.
7
8https://github.com/formiaczek/python_data_structures
9___________________________________
10
11 Copyright (c) 2014 Lukasz Forynski <lukasz.forynski@gmail.com>
12
13Permission is hereby granted, free of charge, to any person obtaining a copy of this
14software and associated documentation files (the "Software"), to deal in the Software
15without restriction, including without limitation the rights to use, copy, modify, merge,
16publish, distribute, sub-license, and/or sell copies of the Software, and to permit persons
17to whom the Software is furnished to do so, subject to the following conditions:
18
19- The above copyright notice and this permission notice shall be included in all copies
20or substantial portions of the Software.
21
22THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
23INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
24PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
25FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
26OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27DEALINGS IN THE SOFTWARE.
28'''
29
30import platform
31_python3 = int(platform.python_version_tuple()[0]) >= 3
32
33class multi_key_dict(object):
34    """ The purpose of this type is to provide a multi-key dictionary.
35    This kind of dictionary has a similar interface to the standard dictionary, and indeed if used
36    with single key key elements - it's behaviour is the same as for a standard dict().
37
38    However it also allows for creation of elements using multiple keys (using tuples/lists).
39    Such elements can be accessed using either of those keys (e.g read/updated/deleted).
40    Dictionary provides also an extended interface for iterating over items and keys by the key type.
41    This can be useful e.g.: when creating dictionaries with (index,name) allowing one to iterate over
42    items using either: names or indexes. It can be useful for many many other similar use-cases,
43    and there is no limit to the number of keys used to map to the value.
44
45    There are also methods to find other keys mapping to the same value as the specified keys etc.
46    Refer to examples and test code to see it in action.
47
48    simple example:
49        k = multi_key_dict()
50        k[100] = 'hundred'  # add item to the dictionary (as for normal dictionary)
51
52        # but also:
53        # below creates entry with two possible key types: int and str,
54        # mapping all keys to the assigned value
55        k[1000, 'kilo', 'k'] = 'kilo (x1000)'
56        print k[1000]   # will print 'kilo (x1000)'
57        print k['k']  # will also print 'kilo (x1000)'
58
59        # the same way objects can be updated, and if an object is updated using one key, the new value will
60        # be accessible using any other key, e.g. for example above:
61        k['kilo'] = 'kilo'
62        print k[1000] # will print 'kilo' as value was updated
63    """
64
65    def __init__(self, mapping_or_iterable=None, **kwargs):
66        """ Initializes dictionary from an optional positional argument and a possibly empty set of keyword arguments."""
67        self.items_dict = {}
68        if mapping_or_iterable is not None:
69            if type(mapping_or_iterable) is dict:
70                mapping_or_iterable = mapping_or_iterable.items()
71            for kv in mapping_or_iterable:
72                if len(kv) != 2:
73                    raise Exception('Iterable should contain tuples with exactly two values but specified: {0}.'.format(kv))
74                self[kv[0]] = kv[1]
75        for keys, value in kwargs.items():
76            self[keys] = value
77
78    def __getitem__(self, key):
79        """ Return the value at index specified as key."""
80        return self.items_dict[self.__dict__[str(type(key))][key]]
81
82    def __setitem__(self, keys, value):
83        """ Set the value at index (or list of indexes) specified as keys.
84            Note, that if multiple key list is specified, either:
85              - none of keys should map to an existing item already (item creation), or
86              - all of keys should map to exactly the same item (as previously created)
87                  (item update)
88            If this is not the case - KeyError is raised. """
89        if(type(keys) in [tuple, list]):
90            at_least_one_key_exists = False
91            num_of_keys_we_have = 0
92
93            for x in keys:
94                try:
95                    self.__getitem__(x)
96                    num_of_keys_we_have += 1
97                except Exception as err:
98                    continue
99
100            if num_of_keys_we_have:
101                all_select_same_item = True
102                direct_key = None
103                for key in keys:
104                    key_type = str(type(key))
105                    try:
106                        if not direct_key:
107                            direct_key = self.__dict__[key_type][key]
108                        else:
109                            new = self.__dict__[key_type][key]
110                            if new != direct_key:
111                                all_select_same_item = False
112                                break
113                    except Exception as err:
114                        all_select_same_item = False
115                        break;
116
117                if not all_select_same_item:
118                    raise KeyError(', '.join(str(key) for key in keys))
119
120            first_key = keys[0] # combination if keys is allowed, simply use the first one
121        else:
122            first_key = keys
123
124        key_type = str(type(first_key)) # find the intermediate dictionary..
125        if first_key in self:
126            self.items_dict[self.__dict__[key_type][first_key]] = value # .. and update the object if it exists..
127        else:
128            if(type(keys) not in [tuple, list]):
129                key = keys
130                keys = [keys]
131            self.__add_item(value, keys) # .. or create it - if it doesn't
132
133    def __delitem__(self, key):
134        """ Called to implement deletion of self[key]."""
135        key_type = str(type(key))
136
137        if (key in self and
138            self.items_dict and
139            (self.__dict__[key_type][key] in self.items_dict) ):
140            intermediate_key = self.__dict__[key_type][key]
141
142            # remove the item in main dictionary
143            del self.items_dict[intermediate_key]
144
145            # and remove all references (if there were other keys)
146            for k in self.get_other_keys(key, True):
147                key_type = str(type(k))
148                if (key_type in self.__dict__ and k in self.__dict__[key_type]):
149                    del self.__dict__[key_type][k]
150
151        else:
152            raise KeyError(key)
153
154    def __contains__(self, key):
155        """ Returns True if this object contains an item referenced by the key."""
156        key_type = str(type(key))
157        if key_type in self.__dict__:
158            if key in self.__dict__[key_type]:
159                return True
160
161        return False
162
163    def has_key(self, key):
164        """ Returns True if this object contains an item referenced by the key."""
165        return key in self
166
167    def get_other_keys(self, key, including_current=False):
168        """ Returns list of other keys that are mapped to the same value as specified key.
169            @param key - key for which other keys should be returned.
170            @param including_current if set to True - key will also appear on this list."""
171        other_keys = []
172        if key in self:
173            other_keys.extend(self.__dict__[str(type(key))][key])
174            if not including_current:
175                other_keys.remove(key)
176        return other_keys
177
178    def iteritems(self, key_type=None, return_all_keys=False):
179        """ Returns an iterator over the dictionary's (key, value) pairs.
180            @param key_type if specified, iterator will be returning only (key,value) pairs for this type of key.
181                   Otherwise (if not specified) ((keys,...), value)
182                   i.e. (tuple of keys, values) pairs for all items in this dictionary will be generated.
183            @param return_all_keys if set to True - tuple of keys is retuned instead of a key of this type."""
184
185        if key_type is None:
186            for item in self.items_dict.items():
187                yield item
188            return
189        used_keys = set()
190        key = str(key_type)
191        if key in self.__dict__:
192            for key, keys in self.__dict__[key].items():
193                if keys in used_keys:
194                    continue
195                used_keys.add(keys)
196                value = self.items_dict[keys]
197                if not return_all_keys:
198                    keys = tuple(k for k in keys if isinstance(k, key_type))
199                yield keys, value
200
201    def iterkeys(self, key_type=None, return_all_keys=False):
202        """ Returns an iterator over the dictionary's keys.
203            @param key_type if specified, iterator for a dictionary of this type will be used.
204                   Otherwise (if not specified) tuples containing all (multiple) keys
205                   for this dictionary will be generated.
206            @param return_all_keys if set to True - tuple of keys is retuned instead of a key of this type."""
207        if(key_type is not None):
208            the_key = str(key_type)
209            if the_key in self.__dict__:
210                for key in self.__dict__[the_key].keys():
211                    if return_all_keys:
212                        yield self.__dict__[the_key][key]
213                    else:
214                        yield key
215        else:
216            for keys in self.items_dict.keys():
217                yield keys
218
219    def itervalues(self, key_type=None):
220        """ Returns an iterator over the dictionary's values.
221            @param key_type if specified, iterator will be returning only values pointed by keys of this type.
222                   Otherwise (if not specified) all values in this dictinary will be generated."""
223        if(key_type is not None):
224            intermediate_key = str(key_type)
225            if intermediate_key in self.__dict__:
226                for direct_key in self.__dict__[intermediate_key].values():
227                    yield self.items_dict[direct_key]
228        else:
229            for value in self.items_dict.values():
230                yield value
231
232    if _python3:
233        items = iteritems
234    else:
235        def items(self, key_type=None, return_all_keys=False):
236            return list(self.iteritems(key_type, return_all_keys))
237        items.__doc__ = iteritems.__doc__
238
239    def keys(self, key_type=None):
240        """ Returns a copy of the dictionary's keys.
241            @param key_type if specified, only keys for this type will be returned.
242                 Otherwise list of tuples containing all (multiple) keys will be returned."""
243        if key_type is not None:
244            intermediate_key = str(key_type)
245            if intermediate_key in self.__dict__:
246                return self.__dict__[intermediate_key].keys()
247        else:
248            all_keys = {} # in order to preserve keys() type (dict_keys for python3)
249            for keys in self.items_dict.keys():
250                all_keys[keys] = None
251            return all_keys.keys()
252
253    def values(self, key_type=None):
254        """ Returns a copy of the dictionary's values.
255            @param key_type if specified, only values pointed by keys of this type will be returned.
256                 Otherwise list of all values contained in this dictionary will be returned."""
257        if(key_type is not None):
258            all_items = {} # in order to preserve keys() type (dict_values for python3)
259            keys_used = set()
260            direct_key = str(key_type)
261            if direct_key in self.__dict__:
262                for intermediate_key in self.__dict__[direct_key].values():
263                    if not intermediate_key in keys_used:
264                        all_items[intermediate_key] = self.items_dict[intermediate_key]
265                        keys_used.add(intermediate_key)
266            return all_items.values()
267        else:
268            return self.items_dict.values()
269
270    def __len__(self):
271        """ Returns number of objects in dictionary."""
272        length = 0
273        if 'items_dict' in self.__dict__:
274            length = len(self.items_dict)
275        return length
276
277    def __add_item(self, item, keys=None):
278        """ Internal method to add an item to the multi-key dictionary"""
279        if(not keys or not len(keys)):
280            raise Exception('Error in %s.__add_item(%s, keys=tuple/list of items): need to specify a tuple/list containing at least one key!'
281                            % (self.__class__.__name__, str(item)))
282        direct_key = tuple(keys) # put all keys in a tuple, and use it as a key
283        for key in keys:
284            key_type = str(type(key))
285
286            # store direct key as a value in an intermediate dictionary
287            if(not key_type in self.__dict__):
288                self.__setattr__(key_type, dict())
289            self.__dict__[key_type][key] = direct_key
290
291            # store the value in the actual dictionary
292            if(not 'items_dict' in self.__dict__):
293                self.items_dict = dict()
294            self.items_dict[direct_key] = item
295
296    def get(self, key, default=None):
297        """ Return the value at index specified as key."""
298        if key in self:
299            return self.items_dict[self.__dict__[str(type(key))][key]]
300        else:
301            return default
302
303    def __str__(self):
304        items = []
305        str_repr = lambda x: '\'%s\'' % x if type(x) == str else str(x)
306        if hasattr(self, 'items_dict'):
307            for (keys, value) in self.items():
308                keys_str = [str_repr(k) for k in keys]
309                items.append('(%s): %s' % (', '.join(keys_str),
310                                           str_repr(value)))
311        dict_str = '{%s}' % ( ', '.join(items))
312        return dict_str
313
314def test_multi_key_dict():
315    contains_all = lambda cont, in_items: not (False in [c in cont for c in in_items])
316
317    m = multi_key_dict()
318    assert( len(m) == 0 ), 'expected len(m) == 0'
319    all_keys = list()
320
321    m['aa', 12, 32, 'mmm'] = 123  # create a value with multiple keys..
322    assert( len(m) == 1 ), 'expected len(m) == 1'
323    all_keys.append(('aa', 'mmm', 32, 12)) # store it for later
324
325    # try retrieving other keys mapped to the same value using one of them
326    res = m.get_other_keys('aa')
327    expected = ['mmm', 32, 12]
328    assert(set(res) == set(expected)), 'get_other_keys(\'aa\'): {0} other than expected: {1} '.format(res, expected)
329
330    # try retrieving other keys mapped to the same value using one of them: also include this key
331    res = m.get_other_keys(32, True)
332    expected = ['aa', 'mmm', 32, 12]
333    assert(set(res) == set(expected)), 'get_other_keys(32): {0} other than expected: {1} '.format(res, expected)
334
335    assert( m.has_key('aa') == True ), 'expected m.has_key(\'aa\') == True'
336    assert( m.has_key('aab') == False ), 'expected m.has_key(\'aab\') == False'
337
338    assert( m.has_key(12) == True ), 'expected m.has_key(12) == True'
339    assert( m.has_key(13) == False ), 'expected m.has_key(13) == False'
340    assert( m.has_key(32) == True ), 'expected m.has_key(32) == True'
341
342    m['something else'] = 'abcd'
343    assert( len(m) == 2 ), 'expected len(m) == 2'
344    all_keys.append(('something else',)) # store for later
345
346    m[23] = 0
347    assert( len(m) == 3 ), 'expected len(m) == 3'
348    all_keys.append((23,)) # store for later
349
350    # check if it's possible to read this value back using either of keys
351    assert( m['aa'] == 123 ), 'expected m[\'aa\'] == 123'
352    assert( m[12] == 123 ), 'expected m[12] == 123'
353    assert( m[32] == 123 ), 'expected m[32] == 123'
354    assert( m['mmm'] == 123 ), 'expected m[\'mmm\'] == 123'
355
356    # now update value and again - confirm it back - using different keys..
357    m['aa'] = 45
358    assert( m['aa'] == 45 ), 'expected m[\'aa\'] == 45'
359    assert( m[12] == 45 ), 'expected m[12] == 45'
360    assert( m[32] == 45 ), 'expected m[32] == 45'
361    assert( m['mmm'] == 45 ), 'expected m[\'mmm\'] == 45'
362
363    m[12] = '4'
364    assert( m['aa'] == '4' ), 'expected m[\'aa\'] == \'4\''
365    assert( m[12] == '4' ), 'expected m[12] == \'4\''
366
367    # test __str__
368    m_str_exp = '{(23): 0, (\'aa\', \'mmm\', 32, 12): \'4\', (\'something else\'): \'abcd\'}'
369    m_str = str(m)
370    assert (len(m_str) > 0), 'str(m) should not be empty!'
371    assert (m_str[0] == '{'), 'str(m) should start with \'{\', but does with \'%c\'' % m_str[0]
372    assert (m_str[-1] == '}'), 'str(m) should end with \'}\', but does with \'%c\'' % m_str[-1]
373
374    # check if all key-values are there as expected. They might be sorted differently
375    def get_values_from_str(dict_str):
376        sorted_keys_and_values = []
377        for k in dict_str.split(', ('):
378            keys, val = k.strip('{}() ').replace(')', '').split(':')
379            keys = tuple(sorted([k.strip() for k in keys.split(',')]))
380            sorted_keys_and_values.append((keys, val))
381        return sorted_keys_and_values
382    exp = get_values_from_str(m_str_exp)
383    act = get_values_from_str(m_str)
384    assert (set(act) == set(exp)), 'str(m) values: \'{0}\' are not {1} '.format(act, exp)
385
386    # try accessing / creating new (keys)-> value mapping whilst one of these
387    # keys already maps to a value in this dictionarys
388    try:
389        m['aa', 'bb'] = 'something new'
390        assert(False), 'Should not allow adding multiple-keys when one of keys (\'aa\') already exists!'
391    except KeyError as err:
392        pass
393
394    # now check if we can get all possible keys (formed in a list of tuples)
395    # each tuple containing all keys)
396    res = sorted([sorted([str(x) for x in k]) for k in m.keys()])
397    expected = sorted([sorted([str(x) for x in k]) for k in all_keys])
398    assert(res == expected), 'unexpected values from m.keys(), got:\n%s\n expected:\n%s' %(res, expected)
399
400    # check default items (which will unpack tupe with key(s) and value)
401    num_of_elements = 0
402    for keys, value in m.items():
403        sorted_keys = sorted([str(k) for k in keys])
404        num_of_elements += 1
405        assert(sorted_keys in expected), 'm.items(): unexpected keys: %s' % (sorted_keys)
406        assert(m[keys[0]] == value), 'm.items(): unexpected value: %s (keys: %s)' % (value, keys)
407    assert(num_of_elements > 0), 'm.items() returned generator that did not produce anything'
408
409    # test default iterkeys()
410    num_of_elements = 0
411    for keys in m.keys():
412        num_of_elements += 1
413        keys_s = sorted([str(k) for k in keys])
414        assert(keys_s in expected), 'm.keys(): unexpected keys: {0}'.format(keys_s)
415
416    assert(num_of_elements > 0), 'm.iterkeys() returned generator that did not produce anything'
417
418    # test iterkeys(int, True): useful to get all info from the dictionary
419    # dictionary is iterated over the type specified, but all keys are returned.
420    num_of_elements = 0
421    for keys in m.iterkeys(int, True):
422        keys_s = sorted([str(k) for k in keys])
423        num_of_elements += 1
424        assert(keys_s in expected), 'm.iterkeys(int, True): unexpected keys: {0}'.format(keys_s)
425    assert(num_of_elements > 0), 'm.iterkeys(int, True) returned generator that did not produce anything'
426
427
428    # test values for different types of keys()
429    expected = set([0, '4'])
430    res = set(m.values(int))
431    assert (res == expected), 'm.values(int) are {0}, but expected: {1}.'.format(res, expected)
432
433    expected = sorted(['4', 'abcd'])
434    res = sorted(m.values(str))
435    assert (res == expected), 'm.values(str) are {0}, but expected: {1}.'.format(res, expected)
436
437    current_values = set([0, '4', 'abcd']) # default (should give all values)
438    res = set(m.values())
439    assert (res == current_values), 'm.values() are {0}, but expected: {1}.'.format(res, current_values)
440
441    #test itervalues() (default) - should return all values. (Itervalues for other types are tested below)
442    vals = set()
443    for value in m.itervalues():
444        vals.add(value)
445    assert (current_values == vals), 'itervalues(): expected {0}, but collected {1}'.format(current_values, vals)
446
447    #test items(int)
448    items_for_int = sorted([((12, 32), '4'), ((23,), 0)])
449    assert (items_for_int == sorted(m.items(int))), 'items(int): expected {0}, but collected {1}'.format(items_for_int,
450                                                                                                     sorted(m.items(int)))
451
452    # test items(str)
453    items_for_str = set([(('aa','mmm'), '4'), (('something else',), 'abcd')])
454    res = set(m.items(str))
455    assert (set(res) == items_for_str), 'items(str): expected {0}, but collected {1}'.format(items_for_str, res)
456
457    # test items() (default - all items)
458    # we tested keys(), values(), and __get_item__ above so here we'll re-create all_items using that
459    all_items = set()
460    keys = m.keys()
461    values = m.values()
462    for k in keys:
463        all_items.add( (tuple(k), m[k[0]]) )
464
465    res = set(m.items())
466    assert (all_items == res), 'items() (all items): expected {0},\n\t\t\t\tbut collected {1}'.format(all_items, res)
467
468    # now test deletion..
469    curr_len = len(m)
470    del m[12]
471    assert( len(m) == curr_len - 1 ), 'expected len(m) == %d' % (curr_len - 1)
472    assert(not m.has_key(12)), 'expected deleted key to no longer be found!'
473
474    # try again
475    try:
476        del m['aa']
477        assert(False), 'cant remove again: item m[\'aa\'] should not exist!'
478    except KeyError as err:
479        pass
480
481    # try to access non-existing
482    try:
483        k =  m['aa']
484        assert(False), 'removed item m[\'aa\'] should not exist!'
485    except KeyError as err:
486        pass
487
488    # try to access non-existing with a different key
489    try:
490        k =  m[12]
491        assert(False), 'removed item m[12] should not exist!'
492    except KeyError as err:
493        pass
494
495    # prepare for other tests (also testing creation of new items)
496    del m
497    m = multi_key_dict()
498    tst_range = list(range(10, 40)) + list(range(50, 70))
499    for i in tst_range:
500        m[i] = i # will create a dictionary, where keys are same as items
501
502    # test items()
503    for key, value in m.items(int):
504        assert(key == (value,)), 'items(int): expected {0}, but received {1}'.format(key, value)
505
506    # test iterkeys()
507    num_of_elements = 0
508    returned_keys = set()
509    for key in m.iterkeys(int):
510        returned_keys.add(key)
511        num_of_elements += 1
512    assert(num_of_elements > 0), 'm.iteritems(int) returned generator that did not produce anything'
513    assert (returned_keys == set(tst_range)), 'iterkeys(int): expected {0}, but received {1}'.format(expected, key)
514
515
516    #test itervalues(int)
517    num_of_elements = 0
518    returned_values  = set()
519    for value in m.itervalues(int):
520        returned_values.add(value)
521        num_of_elements += 1
522    assert (num_of_elements > 0), 'm.itervalues(int) returned generator that did not produce anything'
523    assert (returned_values == set(tst_range)), 'itervalues(int): expected {0}, but received {1}'.format(expected, value)
524
525    # test values(int)
526    res = sorted([x for x in m.values(int)])
527    assert (res == tst_range), 'm.values(int) is not as expected.'
528
529    # test keys()
530    assert (set(m.keys(int)) == set(tst_range)), 'm.keys(int) is not as expected.'
531
532    # test setitem with multiple keys
533    m['xy', 999, 'abcd'] = 'teststr'
534    try:
535        m['xy', 998] = 'otherstr'
536        assert(False), 'creating / updating m[\'xy\', 998] should fail!'
537    except KeyError as err:
538        pass
539
540    # test setitem with multiple keys
541    m['cd'] = 'somethingelse'
542    try:
543        m['cd', 999] = 'otherstr'
544        assert(False), 'creating / updating m[\'cd\', 999] should fail!'
545    except KeyError as err:
546        pass
547
548    m['xy', 999] = 'otherstr'
549    assert (m['xy']  == 'otherstr'), 'm[\'xy\'] is not as expected.'
550    assert (m[999]   == 'otherstr'), 'm[999] is not as expected.'
551    assert (m['abcd'] == 'otherstr'), 'm[\'abcd\'] is not as expected.'
552
553    m['abcd', 'xy']   =  'another'
554    assert (m['xy']  == 'another'), 'm[\'xy\'] is not == \'another\'.'
555    assert (m[999]   == 'another'), 'm[999] is not == \'another\''
556    assert (m['abcd'] == 'another'), 'm[\'abcd\'] is not  == \'another\'.'
557
558    # test get functionality of basic dictionaries
559    m['CanIGet'] = 'yes'
560    assert (m.get('CanIGet') == 'yes')
561    assert (m.get('ICantGet') == None)
562    assert (m.get('ICantGet', "Ok") == "Ok")
563
564    k = multi_key_dict()
565    k['1:12', 1] = 'key_has_:'
566    k.items() # should not cause any problems to have : in key
567    assert (k[1] == 'key_has_:'), 'k[1] is not equal to \'abc:def:ghi\''
568
569    import datetime
570    n = datetime.datetime.now()
571    l = multi_key_dict()
572    l[n] = 'now' # use datetime obj as a key
573
574    #test keys..
575    res = [x for x in l.keys()][0] # for python3 keys() returns dict_keys dictionarly
576    expected = n,
577    assert(expected == res), 'Expected \"{0}\", but got: \"{1}\"'.format(expected, res)
578
579    res = [x for x in l.keys(datetime.datetime)][0]
580    assert(n == res), 'Expected {0} as a key, but got: {1}'.format(n, res)
581
582    res = [x for x in l.values()] # for python3 keys() returns dict_values dictionarly
583    expected = ['now']
584    assert(res == expected), 'Expected values: {0}, but got: {1}'.format(expected, res)
585
586    # test items..
587    exp_items = [((n,), 'now')]
588    r = list(l.items())
589    assert(r == exp_items), 'Expected for items(): tuple of keys: {0}, but got: {1}'.format(r, exp_items)
590    assert(exp_items[0][1] == 'now'), 'Expected for items(): value: {0}, but got: {1}'.format('now',
591                                                                                              exp_items[0][1])
592
593    x = multi_key_dict({('k', 'kilo'):1000, ('M', 'MEGA', 1000000):1000000}, milli=0.01)
594    assert (x['k'] == 1000), 'x[\'k\'] is not equal to 1000'
595    x['kilo'] = 'kilo'
596    assert (x['kilo'] == 'kilo'), 'x[\'kilo\'] is not equal to \'kilo\''
597
598    y = multi_key_dict([(('two', 'duo'), 2), (('one', 'uno'), 1), ('three', 3)])
599
600    assert (y['two'] == 2), 'y[\'two\'] is not equal to 2'
601    y['one'] = 'one'
602    assert (y['one'] == 'one'), 'y[\'one\'] is not equal to \'one\''
603
604    try:
605        y = multi_key_dict([(('two', 'duo'), 2), ('one', 'uno', 1), ('three', 3)])
606        assert(False), 'creating dictionary using iterable with tuples of size > 2 should fail!'
607    except:
608        pass
609
610    print ('All test passed OK!')
611
612__all__ = ["multi_key_dict"]
613
614if __name__ == '__main__':
615    try:
616        test_multi_key_dict()
617    except KeyboardInterrupt:
618        print ('\n(interrupted by user)')
619
620