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