├── LICENSE ├── MANIFEST.in ├── README.md ├── setup.py ├── test_twodict.py └── twodict.py /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include MANIFEST.in 4 | include test_twodict.py 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # twodict (Two Way Ordered Dict) 2 | Simple two way ordered dictionary for Python. 3 | 4 | See [wiki](https://github.com/MrS0m30n3/twodict/wiki) for more information. 5 | 6 | # INSTALLATION 7 | 8 | ### Install From Source 9 | 1. Download & extract source from [here](https://github.com/MrS0m30n3/twodict/archive/1.2.zip) 10 | 2. Change directory into **twodict-1.2/** 11 | 3. Run `sudo python setup.py install` 12 | 13 | ### Install From [Pypi](https://pypi.python.org/pypi/twodict) 14 | 1. Run `sudo pip install twodict` 15 | 16 | # USAGE 17 | ```python 18 | from twodict import TwoWayOrderedDict 19 | 20 | tdict = TwoWayOrderedDict() 21 | tdict['a'] = 1 22 | tdict['b'] = 2 23 | tdict['c'] = 3 24 | 25 | print(tdict['a']) # Outputs 1 26 | print(tdict[1]) # Outputs 'a' 27 | 28 | del tdict[2] 29 | print(tdict) # TwoWayOrderedDict([('a', 1), ('c', 3)]) 30 | ``` 31 | 32 | # AUTHOR 33 | [Sotiris Papadopoulos](https://twitter.com/MrS0m30n3) 34 | 35 | # LICENSE 36 | Unlicense (public domain) 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from distutils.core import setup 5 | 6 | from twodict import __version__, __license__ 7 | 8 | 9 | setup( 10 | author = "Sotiris Papadopoulos", 11 | author_email = "ytubedlg@gmail.com", 12 | name = "twodict", 13 | description = "Simple two way ordered dictionary for Python", 14 | version = __version__, 15 | license = __license__, 16 | url = "https://github.com/MrS0m30n3/twodict", 17 | py_modules = ["twodict"] 18 | ) 19 | -------------------------------------------------------------------------------- /test_twodict.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Contains tests for the twodict module.""" 4 | 5 | import sys 6 | import unittest 7 | 8 | try: 9 | from twodict import ( 10 | TwoWayOrderedDict, 11 | DictItemsView, 12 | DictValuesView, 13 | DictKeysView 14 | ) 15 | except ImportError as error: 16 | print(error) 17 | sys.exit(1) 18 | 19 | 20 | ########## Helpers ########## 21 | 22 | class ExtraAssertions(object): 23 | 24 | MSG = "View: {0} is not equal to the given iterable" 25 | 26 | def assertViewEqualU(self, view, iterable): 27 | """Compare the given view with iterable. Order does NOT count.""" 28 | view_list = list(view) 29 | 30 | for item in view_list: 31 | if item not in iterable: 32 | raise AssertionError(self.MSG.format(view_list)) 33 | 34 | def assertViewEqualO(self, view, iterable): 35 | """Compare the given view with iterable. Order does count.""" 36 | view_list = list(view) 37 | 38 | if view_list != iterable: 39 | raise AssertionError(self.MSG.format(view_list)) 40 | 41 | ############################# 42 | 43 | 44 | class TestInit(unittest.TestCase, ExtraAssertions): 45 | 46 | """Test case for the TwoWayOrderedDict initialization.""" 47 | 48 | def test_init_ordered(self): 49 | tdict = TwoWayOrderedDict([('a', 1), ('b', 2), ('c', 3)]) 50 | self.assertViewEqualO(tdict.items(), [('a', 1), ('b', 2), ('c', 3)]) 51 | 52 | def test_init_unordered_kwargs(self): 53 | tdict = TwoWayOrderedDict(a=1, b=2, c=3) 54 | self.assertViewEqualU(tdict.items(), [('a', 1), ('c', 3), ('b', 2)]) 55 | 56 | def test_init_unordered_dict(self): 57 | tdict = TwoWayOrderedDict({'a': 1, 'b': 2, 'c': 3}) 58 | self.assertViewEqualU(tdict.items(), [('a', 1), ('c', 3), ('b', 2)]) 59 | 60 | 61 | class TestGetItem(unittest.TestCase): 62 | 63 | """Test case for the TwoWayOrderedDict __getitem__ method.""" 64 | 65 | def setUp(self): 66 | self.tdict = TwoWayOrderedDict(a=1, b=2, c=3) 67 | 68 | def test_get_item_by_key(self): 69 | self.assertEqual(self.tdict['a'], 1) 70 | 71 | def test_get_item_by_value(self): 72 | self.assertEqual(self.tdict[1], 'a') 73 | 74 | def test_get_item_not_exist(self): 75 | self.assertRaises(KeyError, self.tdict.__getitem__, 'd') 76 | 77 | 78 | class TestSetItem(unittest.TestSuite): 79 | 80 | """Test suite for the TwoWayOrderedDict __setitem__ method. 81 | 82 | Groups together all the test cases for the __setitem__ method 83 | to organize the code better. 84 | 85 | Test cases are based on the following table: 86 | 87 | .-------.-----------.--------.----------.---------. 88 | | | Not Exist | As Key | As Value | As Both | 89 | :-------+-----------+--------+----------+---------: 90 | | key | 1 | 2 | 3 | 4 | 91 | :-------+-----------+--------+----------+---------: 92 | | value | 1 | 2 | 3 | 4 | 93 | '-------'-----------'--------'----------'---------' 94 | 95 | Available permutations for n=4: n*n => 4*4 = 16 (test-cases) 96 | 97 | Test cases details: 98 | 99 | TestValueNotExist: (1, 1), (2, 1), (3, 1), (4, 1) 100 | TestValueExistAsKey: (1, 2), (2, 2), (3, 2), (4, 2) 101 | TestValueExistAsValue: (1, 3), (2, 3), (3, 3), (4, 3) 102 | TestValueExistAsBoth: (1, 4), (2, 4), (3, 4), (4, 4) 103 | 104 | """ 105 | 106 | TEST_CASES_COUNT = 16 107 | 108 | class TestValueNotExist(unittest.TestCase, ExtraAssertions): 109 | 110 | def setUp(self): 111 | self.tdict = TwoWayOrderedDict([('a', 1), ('b', 'b')]) 112 | self.value = 3 113 | 114 | # Hold the expected dict for each test case. Used on tearDown 115 | self.expected_dict = {} 116 | 117 | def test_set_item_key_not_exist(self): 118 | self.tdict['c'] = self.value 119 | self.assertViewEqualO(self.tdict.items(), [('a', 1), ('b', 'b'), ('c', 3)]) 120 | 121 | self.expected_dict = {'a': 1, 1: 'a', 'b': 'b', 'c': 3, 3: 'c'} 122 | 123 | def test_set_item_key_exist_as_key(self): 124 | self.tdict['a'] = self.value 125 | self.assertViewEqualO(self.tdict.items(), [('a', 3), ('b', 'b')]) 126 | 127 | self.expected_dict = {'a': 3, 3: 'a', 'b': 'b'} 128 | 129 | def test_set_item_key_exist_as_value(self): 130 | self.tdict[1] = self.value 131 | self.assertViewEqualO(self.tdict.items(), [('b', 'b'), (1, 3)]) 132 | 133 | self.expected_dict = {'b': 'b', 1: 3, 3: 1} 134 | 135 | def test_set_item_key_exist_as_both(self): 136 | self.tdict['b'] = self.value 137 | self.assertViewEqualO(self.tdict.items(), [('a', 1), ('b', 3)]) 138 | 139 | self.expected_dict = {'a': 1, 1: 'a', 'b': 3, 3: 'b'} 140 | 141 | 142 | class TestValueExistAsKey(unittest.TestCase, ExtraAssertions): 143 | 144 | def setUp(self): 145 | self.tdict = TwoWayOrderedDict([('a', 1), ('b', 'b'), ('c', 3)]) 146 | self.value = 'a' 147 | 148 | # Hold the expected dict for each test case. Used on tearDown 149 | self.expected_dict = {} 150 | 151 | def test_set_item_key_not_exist(self): 152 | self.tdict['d'] = self.value 153 | self.assertViewEqualO(self.tdict.items(), [('b', 'b'), ('c', 3), ('d', 'a')]) 154 | 155 | self.expected_dict = {'b': 'b', 'c': 3, 3: 'c', 'd': 'a', 'a': 'd'} 156 | 157 | def test_set_item_key_exist_as_key(self): 158 | self.tdict['a'] = self.value 159 | self.assertViewEqualO(self.tdict.items(), [('a', 'a'), ('b', 'b'), ('c', 3)]) 160 | 161 | self.expected_dict = {'a': 'a', 'b': 'b', 'c': 3, 3: 'c'} 162 | 163 | def test_set_item_key_exist_as_value(self): 164 | self.tdict[1] = self.value 165 | self.assertViewEqualO(self.tdict.items(), [('b', 'b'), ('c', 3), (1, 'a')]) 166 | 167 | self.expected_dict = {'b': 'b', 'c': 3, 3: 'c', 1: 'a', 'a': 1} 168 | 169 | def test_set_item_key_exist_as_both(self): 170 | self.tdict['b'] = self.value 171 | self.assertViewEqualO(self.tdict.items(), [('b', 'a'), ('c', 3)]) 172 | 173 | self.expected_dict = {'b': 'a', 'a': 'b', 'c': 3, 3: 'c'} 174 | 175 | 176 | class TestValueExistAsValue(unittest.TestCase, ExtraAssertions): 177 | 178 | def setUp(self): 179 | self.tdict = TwoWayOrderedDict([('a', 1), ('b', 'b'), ('c', 3)]) 180 | self.value = 1 181 | 182 | # Hold the expected dict for each test case. Used on tearDown 183 | self.expected_dict = {} 184 | 185 | def test_set_item_key_not_exist(self): 186 | self.tdict['d'] = self.value 187 | self.assertViewEqualO(self.tdict.items(), [('b', 'b'), ('c', 3), ('d', 1)]) 188 | 189 | self.expected_dict = {'b': 'b', 'c': 3, 3: 'c', 'd': 1, 1: 'd'} 190 | 191 | def test_set_item_key_exist_as_key(self): 192 | self.tdict['c'] = self.value 193 | self.assertViewEqualO(self.tdict.items(), [('b', 'b'), ('c', 1)]) 194 | 195 | self.expected_dict = {'b': 'b', 'c': 1, 1: 'c'} 196 | 197 | def test_set_item_key_exist_as_value(self): 198 | self.tdict[1] = self.value 199 | self.assertViewEqualO(self.tdict.items(), [('b', 'b'), ('c', 3), (1, 1)]) 200 | 201 | self.expected_dict = {'b': 'b', 'c': 3, 3: 'c', 1: 1} 202 | 203 | def test_set_item_key_exist_as_both(self): 204 | self.tdict['b'] = self.value 205 | self.assertViewEqualO(self.tdict.items(), [('b', 1), ('c', 3)]) 206 | 207 | self.expected_dict = {'b': 1, 1: 'b', 'c': 3, 3: 'c'} 208 | 209 | 210 | class TestValueExistAsBoth(unittest.TestCase, ExtraAssertions): 211 | 212 | def setUp(self): 213 | self.tdict = TwoWayOrderedDict([('a', 1), ('b', 'b'), ('c', 3)]) 214 | self.value = 'b' 215 | 216 | # Hold the expected dict for each test case. Used on tearDown 217 | self.expected_dict = {} 218 | 219 | def test_set_item_key_not_exist(self): 220 | self.tdict['d'] = self.value 221 | self.assertViewEqualO(self.tdict.items(), [('a', 1), ('c', 3), ('d', 'b')]) 222 | 223 | self.expected_dict = {'a': 1, 1: 'a', 'c': 3, 3: 'c', 'd': 'b', 'b': 'd'} 224 | 225 | def test_set_item_key_exist_as_key(self): 226 | self.tdict['a'] = self.value 227 | self.assertViewEqualO(self.tdict.items(), [('a', 'b'), ('c', 3)]) 228 | 229 | self.expected_dict = {'a': 'b', 'b': 'a', 'c': 3, 3: 'c'} 230 | 231 | def test_set_item_key_exist_as_value(self): 232 | self.tdict[1] = self.value 233 | self.assertViewEqualO(self.tdict.items(), [('c', 3), (1, 'b')]) 234 | 235 | self.expected_dict = {'c': 3, 3: 'c', 1: 'b', 'b': 1} 236 | 237 | def test_set_item_key_exist_as_both(self): 238 | self.tdict['b'] = self.value 239 | self.assertViewEqualO(self.tdict.items(), [('a', 1), ('b', 'b'), ('c', 3)]) 240 | 241 | self.expected_dict = {'a': 1, 1: 'a', 'b': 'b', 'c': 3, 3: 'c'} 242 | 243 | 244 | def __init__(self): 245 | super(TestSetItem, self).__init__() 246 | test_loader = unittest.TestLoader() 247 | 248 | test_cases = [ 249 | self.TestValueNotExist, 250 | self.TestValueExistAsKey, 251 | self.TestValueExistAsValue, 252 | self.TestValueExistAsBoth 253 | ] 254 | 255 | for test_case in test_cases: 256 | # Overwrite the tearDown method for each test case 257 | test_case.tearDown = self.tearDownForTestCases 258 | 259 | self.addTests(test_loader.loadTestsFromTestCase(test_case)) 260 | 261 | assert self.countTestCases() == self.TEST_CASES_COUNT 262 | 263 | @staticmethod 264 | def tearDownForTestCases(test_case): 265 | """Test the status of the parent dictionary.""" 266 | current_dict = super(test_case.tdict.__class__, test_case.tdict).copy() 267 | expected_dict = test_case.expected_dict 268 | 269 | test_case.assertEqual(current_dict, expected_dict) 270 | 271 | 272 | class TestDelItem(unittest.TestCase, ExtraAssertions): 273 | 274 | """Test case for the TwoWayOrderedDict __delitem__ method.""" 275 | 276 | def setUp(self): 277 | self.tdict = TwoWayOrderedDict([('a', 1), ('b', 'b'), ('c', 3)]) 278 | 279 | def test_del_item_by_key(self): 280 | del self.tdict['a'] 281 | self.assertViewEqualO(self.tdict.items(), [('b', 'b'), ('c', 3)]) 282 | 283 | def test_del_item_by_value(self): 284 | del self.tdict[3] 285 | self.assertViewEqualO(self.tdict.items(), [('a', 1), ('b', 'b')]) 286 | 287 | def test_del_item_not_exist(self): 288 | self.assertRaises(KeyError, self.tdict.__delitem__, 'd') 289 | 290 | def test_del_item_key_equals_value(self): 291 | del self.tdict['b'] 292 | self.assertViewEqualO(self.tdict.items(), [('a', 1), ('c', 3)]) 293 | 294 | 295 | class TestLength(unittest.TestCase): 296 | 297 | """Test case for the TwoWayOrderedDict __len__ method.""" 298 | 299 | def test_length_empty(self): 300 | tdict = TwoWayOrderedDict() 301 | self.assertEqual(len(tdict), 0) 302 | 303 | def test_length_not_empty(self): 304 | tdict = TwoWayOrderedDict(a=1, b=2, c=3) 305 | self.assertEqual(len(tdict), 3) 306 | 307 | 308 | class TestIteration(unittest.TestCase): 309 | 310 | """Test case for the TwoWayOrderedDict __iter__ & __reversed__ methods.""" 311 | 312 | KEY_INDEX = 0 313 | 314 | def setUp(self): 315 | self.items = [('a', 1), ('b', 2), ('c', 3)] 316 | self.tdict = TwoWayOrderedDict(self.items) 317 | 318 | def test_iter(self): 319 | for index, key in enumerate(self.tdict): 320 | self.assertEqual(key, self.items[index][self.KEY_INDEX]) 321 | 322 | def test_iter_reversed(self): 323 | for index, key in enumerate(reversed(self.tdict)): 324 | reversed_index = (index + 1) * (-1) 325 | self.assertEqual(key, self.items[reversed_index][self.KEY_INDEX]) 326 | 327 | 328 | class TestComparison(unittest.TestCase): 329 | 330 | """Test case for the TwoWayOrderedDict compare methods.""" 331 | 332 | def test_equal(self): 333 | t1 = TwoWayOrderedDict(a=1, b=2, c=3) 334 | t2 = TwoWayOrderedDict(a=1, b=2, c=3) 335 | 336 | self.assertEqual(t1, t2) 337 | 338 | def test_not_equal(self): 339 | t1 = TwoWayOrderedDict(a=1, b=2, c=3) 340 | t2 = TwoWayOrderedDict(a=1, b=2, d=3) 341 | 342 | self.assertNotEqual(t1, t2) 343 | 344 | 345 | class TestGetValuesAndKeys(unittest.TestCase, ExtraAssertions): 346 | 347 | """Test case for the TwoWayOrderedDict values() & keys() methods.""" 348 | 349 | def setUp(self): 350 | self.tdict = TwoWayOrderedDict([('a', 1), ('b', 2), ('c', 3)]) 351 | self.tdict_empty = TwoWayOrderedDict() 352 | 353 | def test_get_keys_empty(self): 354 | self.assertViewEqualO(self.tdict_empty.keys(), []) 355 | 356 | def test_get_keys_not_empty(self): 357 | self.assertViewEqualO(self.tdict.keys(), ['a', 'b', 'c']) 358 | 359 | def test_get_values_empty(self): 360 | self.assertViewEqualO(self.tdict_empty.values(), []) 361 | 362 | def test_get_values_not_empty(self): 363 | self.assertViewEqualO(self.tdict.values(), [1, 2, 3]) 364 | 365 | 366 | class TestPopMethods(unittest.TestCase, ExtraAssertions): 367 | 368 | """Test case for the TwoWayOrderedDict pop() & popitem() methods.""" 369 | 370 | def setUp(self): 371 | self.tdict = TwoWayOrderedDict([('a', 1), ('b', 2), ('c', 3)]) 372 | 373 | def test_pop_by_key(self): 374 | self.assertEqual(self.tdict.pop('a'), 1) 375 | self.assertViewEqualO(self.tdict.items(), [('b', 2), ('c', 3)]) 376 | 377 | def test_pop_by_value(self): 378 | self.assertEqual(self.tdict.pop(1), 'a') 379 | self.assertViewEqualO(self.tdict.items(), [('b', 2), ('c', 3)]) 380 | 381 | def test_pop_raises(self): 382 | self.assertRaises(KeyError, self.tdict.pop, 'd') 383 | 384 | def test_pop_with_default_value(self): 385 | self.assertIsNone(self.tdict.pop('d', None)) 386 | 387 | def test_popitem_last(self): 388 | self.assertEqual(self.tdict.popitem(), ('c', 3)) 389 | self.assertViewEqualO(self.tdict.items(), [('a', 1), ('b', 2)]) 390 | 391 | def test_popitem_first(self): 392 | self.assertEqual(self.tdict.popitem(last=False), ('a', 1)) 393 | self.assertViewEqualO(self.tdict.items(), [('b', 2), ('c', 3)]) 394 | 395 | def test_popitem_raises(self): 396 | while self.tdict: self.tdict.popitem() 397 | 398 | self.assertRaises(KeyError, self.tdict.popitem) 399 | 400 | 401 | class TestUpdate(unittest.TestCase, ExtraAssertions): 402 | 403 | """Test case for the TwoWayOrderedDict update method.""" 404 | 405 | def setUp(self): 406 | self.tdict = TwoWayOrderedDict([('a', 1), ('b', 2)]) 407 | 408 | def test_update_ordered(self): 409 | self.tdict.update([('a', 10), ('c', 3), ('d', 4), ('e', 5)]) 410 | self.assertViewEqualO(self.tdict.items(), [('a', 10), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]) 411 | 412 | def test_update_unordered(self): 413 | self.tdict.update({'a': 10, 'c': 3, 'd': 4, 'e': 5}) 414 | self.assertViewEqualU(self.tdict.items(), [('a', 10), ('b', 2), ('c', 3), ('e', 5), ('d', 4)]) 415 | 416 | def test_update_raises(self): 417 | self.assertRaises(TypeError, self.tdict.update, [('a', 10)], [('b', 20)]) 418 | 419 | def test_update_from_other(self): 420 | other = TwoWayOrderedDict([('a', 10), ('b', 20), ('c', 30)]) 421 | self.tdict.update(other) 422 | self.assertViewEqualO(self.tdict.items(), [('a', 10), ('b', 20), ('c', 30)]) 423 | 424 | 425 | class TestSetDefault(unittest.TestCase, ExtraAssertions): 426 | 427 | """Test case for the TwoWayOrderedDict setdefault method.""" 428 | 429 | def setUp(self): 430 | self.tdict = TwoWayOrderedDict([('a', 1), ('b', 2)]) 431 | 432 | def test_setdefault_by_key(self): 433 | self.assertEqual(self.tdict.setdefault('a'), 1) 434 | 435 | def test_setdefault_by_value(self): 436 | self.assertEqual(self.tdict.setdefault(1), 'a') 437 | 438 | def test_setdefault_not_exist(self): 439 | self.assertIsNone(self.tdict.setdefault('c')) 440 | self.assertEqual(self.tdict.setdefault('d', 'd'), 'd') 441 | 442 | self.assertViewEqualO(self.tdict.items(), [('a', 1), ('b', 2), ('c', None), ('d', 'd')]) 443 | 444 | 445 | class TestCopy(unittest.TestCase): 446 | 447 | """Test case for the TwoWayOrderedDict copy method.""" 448 | 449 | def test_copy(self): 450 | tdict = TwoWayOrderedDict([('a', 1), ('b', 2)]) 451 | 452 | tdict_copy = tdict.copy() 453 | self.assertEqual(tdict, tdict_copy) 454 | 455 | tdict_copy['c'] = 3 456 | self.assertNotEqual(tdict, tdict_copy) 457 | 458 | 459 | class TestClear(unittest.TestCase): 460 | 461 | """Test case for the TwoWayOrderedDict clear method.""" 462 | 463 | def test_clear_empty(self): 464 | tdict = TwoWayOrderedDict() 465 | tdict.clear() 466 | self.assertEqual(len(tdict), 0) 467 | 468 | def test_clear_not_empty(self): 469 | tdict = TwoWayOrderedDict(a=1, b=2, c=3) 470 | tdict.clear() 471 | self.assertEqual(len(tdict), 0) 472 | 473 | 474 | @unittest.skipIf(sys.version_info >= (3, 0) or sys.version_info < (2, 2), 475 | "Current Python version does not support this methods") 476 | class TestOldMethods(unittest.TestCase): 477 | 478 | """Contains test cases for the deprecated methods. 479 | 480 | Deprecated methods: 481 | iteritems(): Replaced by items(). 482 | 483 | iterkeys(): Replaced by keys(). 484 | 485 | itervalues(): Replaced by values(). 486 | 487 | viewitems(): Replaced by items(). 488 | 489 | viewkeys(): Replaced by keys(). 490 | 491 | viewvalues(): Replaced by values(). 492 | 493 | Note: 494 | In Python 3.* those methods are not available. 495 | 496 | """ 497 | 498 | def setUp(self): 499 | self.tdict = TwoWayOrderedDict() 500 | 501 | def test_iteritems_raises(self): 502 | self.assertRaises(NotImplementedError, self.tdict.iteritems) 503 | 504 | def test_iterkeys_raises(self): 505 | self.assertRaises(NotImplementedError, self.tdict.iterkeys) 506 | 507 | def test_itervalues_raises(self): 508 | self.assertRaises(NotImplementedError, self.tdict.itervalues) 509 | 510 | def test_viewitems_raises(self): 511 | self.assertRaises(NotImplementedError, self.tdict.viewitems) 512 | 513 | def test_viewkeys_raises(self): 514 | self.assertRaises(NotImplementedError, self.tdict.viewkeys) 515 | 516 | def test_viewvalues_raises(self): 517 | self.assertRaises(NotImplementedError, self.tdict.viewvalues) 518 | 519 | 520 | ########## DictViews section ########## 521 | 522 | 523 | class TestDictKeysView(unittest.TestCase): 524 | 525 | """Contains all the test cases for the DictKeysView object.""" 526 | 527 | def setUp(self): 528 | self.tdict = TwoWayOrderedDict([('a', 1), ('b', 2)]) 529 | 530 | self.keys_view = DictKeysView(self.tdict) 531 | self.keys = ['a', 'b'] 532 | 533 | def test_length(self): 534 | self.assertEqual(len(self.keys_view), len(self.keys)) 535 | 536 | def test_iter(self): 537 | for index, key in enumerate(self.keys_view): 538 | self.assertEqual(key, self.keys[index]) 539 | 540 | def test_contains(self): 541 | self.assertIn('a', self.keys_view) 542 | self.assertNotIn(1, self.keys_view) 543 | 544 | def test_repr(self): 545 | self.assertEqual(repr(self.keys_view), "dict_keys(['a', 'b'])") 546 | 547 | 548 | class TestDictValuesView(unittest.TestCase): 549 | 550 | """Contains all the test cases for the DictValuesView object.""" 551 | 552 | def setUp(self): 553 | self.tdict = TwoWayOrderedDict([('a', 1), ('b', 2)]) 554 | 555 | self.values_view = DictValuesView(self.tdict) 556 | self.values = [1, 2] 557 | 558 | def test_length(self): 559 | self.assertEqual(len(self.values_view), len(self.values)) 560 | 561 | def test_iter(self): 562 | for index, value in enumerate(self.values_view): 563 | self.assertEqual(value, self.values[index]) 564 | 565 | def test_contains(self): 566 | self.assertIn(1, self.values_view) 567 | self.assertNotIn('a', self.values_view) 568 | 569 | def test_repr(self): 570 | self.assertEqual(repr(self.values_view), "dict_values([1, 2])") 571 | 572 | 573 | class TestDictItemsView(unittest.TestCase): 574 | 575 | """Contains all the test cases for the DictItemsView object.""" 576 | 577 | def setUp(self): 578 | self.tdict = TwoWayOrderedDict([('a', 1), ('b', 2)]) 579 | 580 | self.items_view = DictItemsView(self.tdict) 581 | self.items = [('a', 1), ('b', 2)] 582 | 583 | def test_length(self): 584 | self.assertEqual(len(self.items_view), len(self.items)) 585 | 586 | def test_iter(self): 587 | for index, item in enumerate(self.items_view): 588 | self.assertEqual(item, self.items[index]) 589 | 590 | def test_contains(self): 591 | self.assertIn(('a', 1), self.items_view) 592 | self.assertNotIn('a', self.items_view) 593 | self.assertNotIn(1, self.items_view) 594 | self.assertNotIn(('a', 10), self.items_view) 595 | 596 | def test_repr(self): 597 | self.assertEqual(repr(self.items_view), "dict_items([('a', 1), ('b', 2)])") 598 | 599 | 600 | def all_tests_suite(): 601 | """Return a test suite with all TestCases - TestSuites in this module.""" 602 | test_cases_list = [ 603 | TestInit, 604 | TestGetItem, 605 | TestDelItem, 606 | TestLength, 607 | TestIteration, 608 | TestComparison, 609 | TestGetValuesAndKeys, 610 | TestPopMethods, 611 | TestUpdate, 612 | TestSetDefault, 613 | TestCopy, 614 | TestClear, 615 | TestOldMethods, 616 | TestDictKeysView, 617 | TestDictValuesView, 618 | TestDictItemsView 619 | ] 620 | 621 | test_suites_list = [ 622 | TestSetItem 623 | ] 624 | 625 | 626 | suite = unittest.TestSuite() 627 | loader = unittest.TestLoader() 628 | 629 | for test_case in test_cases_list: 630 | tests = loader.loadTestsFromTestCase(test_case) 631 | suite.addTest(tests) 632 | 633 | for test_suite in test_suites_list: 634 | suite.addTest(test_suite()) 635 | 636 | return suite 637 | 638 | 639 | def main(): 640 | verb_lvl = 2 if "-v" in sys.argv else 1 641 | failfast_sts = True if "-f" in sys.argv else False 642 | 643 | runner = unittest.TextTestRunner(verbosity=verb_lvl, failfast=failfast_sts) 644 | runner.run(all_tests_suite()) 645 | 646 | #unittest.main() 647 | 648 | 649 | if __name__ == "__main__": 650 | main() 651 | -------------------------------------------------------------------------------- /twodict.py: -------------------------------------------------------------------------------- 1 | """Two Way Ordered DICTionary for Python. 2 | 3 | Attributes: 4 | _DEFAULT_OBJECT (object): Object that it's used as a default parameter. 5 | 6 | """ 7 | 8 | import sys 9 | import collections 10 | 11 | 12 | __all__ = ["TwoWayOrderedDict"] 13 | 14 | __version__ = "1.2" 15 | 16 | __license__ = "Unlicense" 17 | 18 | 19 | _DEFAULT_OBJECT = object() 20 | 21 | 22 | ########## Custom views to mimic Python3 view objects ########## 23 | # See: https://docs.python.org/3/library/stdtypes.html#dict-views 24 | 25 | class DictKeysView(collections.KeysView): 26 | 27 | def __init__(self, data): 28 | super(DictKeysView, self).__init__(data) 29 | self.__data = data 30 | 31 | def __repr__(self): 32 | return "dict_keys({data})".format(data=list(self)) 33 | 34 | def __contains__(self, key): 35 | return key in [key for key in self.__data] 36 | 37 | 38 | class DictValuesView(collections.ValuesView): 39 | 40 | def __init__(self, data): 41 | super(DictValuesView, self).__init__(data) 42 | self.__data = data 43 | 44 | def __repr__(self): 45 | return "dict_values({data})".format(data=list(self)) 46 | 47 | def __contains__(self, value): 48 | return value in [self.__data[key] for key in self.__data] 49 | 50 | 51 | class DictItemsView(collections.ItemsView): 52 | 53 | def __init__(self, data): 54 | super(DictItemsView, self).__init__(data) 55 | self.__data = data 56 | 57 | def __repr__(self): 58 | return "dict_items({data})".format(data=list(self)) 59 | 60 | def __contains__(self, item): 61 | return item in [(key, self.__data[key]) for key in self.__data] 62 | 63 | ########################################################### 64 | 65 | 66 | class TwoWayOrderedDict(dict): 67 | 68 | """Custom data structure which implements a two way ordered dictionary. 69 | 70 | Custom dictionary that supports key:value relationships AND value:key 71 | relationships. It also remembers the order in which the items were inserted 72 | and supports almost all the features of the build-in dict. 73 | 74 | Examples: 75 | Unordered initialization:: 76 | 77 | >>> tdict = TwoWayOrderedDict(a=1, b=2, c=3) 78 | 79 | Ordered initialization:: 80 | 81 | >>> tdict = TwoWayOrderedDict([('a', 1), ('b', 2), ('c', 3)]) 82 | 83 | Simple usage:: 84 | 85 | >>> tdict = TwoWayOrderedDict() 86 | >>> tdict['a'] = 1 87 | >>> tdict['b'] = 2 88 | >>> tdict['c'] = 3 89 | 90 | >>> tdict['a'] # Outputs 1 91 | >>> tdict[1] # Outputs 'a' 92 | 93 | >>> del tdict[2] 94 | 95 | >>> print(tdict) 96 | TwoWayOrderedDict([('a', 1), ('c', 3)]) 97 | 98 | """ 99 | 100 | _PREV = 0 101 | _KEY = 1 102 | _NEXT = 2 103 | 104 | def __init__(self, *args, **kwargs): 105 | super(TwoWayOrderedDict, self).__init__() 106 | 107 | self.clear() 108 | self.update(*args, **kwargs) 109 | 110 | def __setitem__(self, key, value): 111 | if key in self: 112 | # Make sure that key != self[key] before removing self[key] from 113 | # our linked list because we will lose the order 114 | # For example {'a': 'a'} and we do d['a'] = 2 115 | if key != self[key]: 116 | self._remove_mapped_key(self[key]) 117 | 118 | dict.__delitem__(self, self[key]) 119 | 120 | if value in self: 121 | # Make sure that key != value before removing value from our 122 | # linked list because we will lose the order if we remove 123 | # value = key from our linked list 124 | if key != value: 125 | self._remove_mapped_key(value) 126 | 127 | self._remove_mapped_key(self[value]) 128 | 129 | # Check if self[value] is in the dict in case that the 130 | # first del (line:117) has already removed the self[value] 131 | # For example {'a': 1, 1: 'a'} and we do d['a'] = 'a' 132 | if self[value] in self: 133 | dict.__delitem__(self, self[value]) 134 | 135 | if key not in self._items_map: 136 | last = self._items[self._PREV] 137 | last[self._NEXT] = self._items[self._PREV] = self._items_map[key] = [last, key, self._items] 138 | 139 | dict.__setitem__(self, key, value) 140 | dict.__setitem__(self, value, key) 141 | 142 | def __delitem__(self, key): 143 | self._remove_mapped_key(self[key]) 144 | self._remove_mapped_key(key) 145 | 146 | dict.__delitem__(self, self[key]) 147 | 148 | # Cases like {'a': 'a'} where we have only one copy instead of {'a': 1, 1: 'a'} 149 | if key in self: 150 | dict.__delitem__(self, key) 151 | 152 | def __len__(self): 153 | return len(self._items_map) 154 | 155 | def __iter__(self): 156 | return self._iterate() 157 | 158 | def __reversed__(self): 159 | return self._iterate(reverse=True) 160 | 161 | def __repr__(self): 162 | return '%s(%r)' % (self.__class__.__name__, list(self.items())) 163 | 164 | def __eq__(self, other): 165 | if not isinstance(other, self.__class__): 166 | return False 167 | 168 | return self.items() == other.items() 169 | 170 | def __ne__(self, other): 171 | return not self == other 172 | 173 | def _remove_mapped_key(self, key): 174 | """Remove the given key both from the linked list and the items map.""" 175 | if key in self._items_map: 176 | prev_item, _, next_item = self._items_map.pop(key) 177 | prev_item[self._NEXT] = next_item 178 | next_item[self._PREV] = prev_item 179 | 180 | def _iterate(self, reverse=False): 181 | """Generator that iterates over the dictionary keys.""" 182 | index = self._PREV if reverse else self._NEXT 183 | curr = self._items[index] 184 | 185 | while curr is not self._items: 186 | yield curr[self._KEY] 187 | curr = curr[index] 188 | 189 | def items(self): 190 | return DictItemsView(self) 191 | 192 | def values(self): 193 | return DictValuesView(self) 194 | 195 | def keys(self): 196 | return DictKeysView(self) 197 | 198 | def pop(self, key, default=_DEFAULT_OBJECT): 199 | try: 200 | value = self[key] 201 | 202 | del self[key] 203 | except KeyError as error: 204 | if default == _DEFAULT_OBJECT: 205 | raise error 206 | 207 | value = default 208 | 209 | return value 210 | 211 | def popitem(self, last=True): 212 | """Remove and return a (key, value) pair from the dictionary. 213 | 214 | Args: 215 | last (boolean): When True popitem() will remove the last list item. 216 | When False popitem() will remove the first list item. 217 | 218 | Note: 219 | popitem() is useful to destructively iterate over a dictionary. 220 | 221 | Raises: 222 | KeyError: If the dictionary is empty. 223 | 224 | """ 225 | if not self: 226 | raise KeyError('popitem(): dictionary is empty') 227 | 228 | index = self._PREV if last else self._NEXT 229 | 230 | _, key, _ = self._items[index] 231 | value = self.pop(key) 232 | 233 | return key, value 234 | 235 | def update(self, *args, **kwargs): 236 | if len(args) > 1: 237 | raise TypeError("expected at most 1 arguments, got {0}".format(len(args))) 238 | 239 | for item in args: 240 | if isinstance(item, dict): 241 | item = item.items() 242 | 243 | for key, value in item: 244 | self[key] = value 245 | 246 | for key, value in kwargs.items(): 247 | self[key] = value 248 | 249 | def setdefault(self, key, default=None): 250 | try: 251 | return self[key] 252 | except KeyError: 253 | self[key] = default 254 | return default 255 | 256 | def copy(self): 257 | return self.__class__(self.items()) 258 | 259 | def clear(self): 260 | self._items = item = [] 261 | # Cycled double linked list [previous, key, next] 262 | self._items += [item, None, item] 263 | # Map linked list items into keys to speed up lookup 264 | self._items_map = {} 265 | dict.clear(self) 266 | 267 | @staticmethod 268 | def __not_implemented(): 269 | raise NotImplementedError("Please use the equivalent items(), keys(), values() methods") 270 | 271 | if sys.version_info < (3, 0) and sys.version_info >= (2, 2): 272 | iteritems = iterkeys = itervalues = viewitems = viewkeys = viewvalues = __not_implemented 273 | --------------------------------------------------------------------------------