Index: Doc/library/collections.rst =================================================================== --- Doc/library/collections.rst (revision 70081) +++ Doc/library/collections.rst (working copy) @@ -14,7 +14,7 @@ __name__ = '' This module implements high-performance container datatypes. Currently, -there are three datatypes, :class:`Counter`, :class:`deque` and +there are four datatypes, :class:`Counter`, :class:`deque`, :class:`OrderedDict` and :class:`defaultdict`, and one datatype factory function, :func:`namedtuple`. The specialized containers provided in this module provide alternatives @@ -806,7 +806,29 @@ adapted for Python 2.4. +:class:`OrderedDict` objects +---------------------------- +Ordered dictionaries are just like regular dictionaries but they remember the +order that items were inserted. When iterating over an ordered dictionary, +the items are returned in the order they were first added. The +:meth:`popitem` method removes entries from the last added to the first (in +LIFO order). + +.. class:: OrderedDict([items]) + + Return an instance of a dict subclass, supporting the usual :class:`dict` + methods. An *OrderedDict* remembers order that entries were inserted. + If a new entry overwrites an existing entry, the original insertion + position is left unchanged. + +Since :class:`OrderDict` objects are :class:`dict` subclasses, their equality +comparison operation is the same as for regular dictionaries and does not +take into account the insertion order. If an order-sensitive comparison is +needed, just compare their ordered :attr:`items` list: +``list(od1.items())==list(od2.items())``. + + :class:`UserDict` objects ------------------------- Index: Lib/collections.py =================================================================== --- Lib/collections.py (revision 70081) +++ Lib/collections.py (working copy) @@ -1,5 +1,5 @@ __all__ = ['deque', 'defaultdict', 'namedtuple', 'UserDict', 'UserList', - 'UserString', 'Counter'] + 'UserString', 'Counter', 'OrderedDict'] # For bootstrapping reasons, the collection ABCs are defined in _abcoll.py. # They should however be considered an integral part of collections.py. from _abcoll import * @@ -13,7 +13,75 @@ import heapq as _heapq from itertools import repeat as _repeat, chain as _chain, starmap as _starmap +################################################################################ +### OrderedDict +################################################################################ +class OrderedDict(dict, MutableMapping): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at 1 argument, got %d', len(args)) + if not hasattr(self, '_keys'): + self._keys = [] + self.update(*args, **kwds) + + def clear(self): + del self._keys[:] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + self._keys.append(key) + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + self._keys.remove(key) + + def __iter__(self): + return iter(self._keys) + + def __reversed__(self): + return reversed(self._keys) + + def popitem(self): + if not self: + raise KeyError + key = self._keys.pop() + value = dict.pop(self, key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + inst_dict = vars(self).copy() + inst_dict.pop('_keys', None) + return (self.__class__, (items,), inst_dict) + + setdefault = MutableMapping.setdefault + update = MutableMapping.update + pop = MutableMapping.pop + keys = MutableMapping.keys + values = MutableMapping.values + items = MutableMapping.items + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + pairs = ', '.join(map('%r: %r'.__mod__, self.items())) + return '%s({%s})' % (self.__class__.__name__, pairs) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + ################################################################################ ### namedtuple ################################################################################ Index: Lib/test/test_collections.py =================================================================== --- Lib/test/test_collections.py (revision 70081) +++ Lib/test/test_collections.py (working copy) @@ -1,10 +1,12 @@ """Unit tests for collections.py.""" import unittest, doctest +import inspect from test import support -from collections import namedtuple, Counter, Mapping +from collections import namedtuple, Counter, OrderedDict +from test import mapping_tests import pickle, copy -from random import randrange +from random import randrange, shuffle import operator from collections import Hashable, Iterable, Iterator from collections import Sized, Container, Callable @@ -571,12 +573,160 @@ set_result = setop(set(p.elements()), set(q.elements())) self.assertEqual(counter_result, dict.fromkeys(set_result, 1)) + +class TestOrderedDict(unittest.TestCase): + + def setUp(self): + self.pairs = [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)] + self.od = OrderedDict(self.pairs) + + def test_init(self): + with self.assertRaises(TypeError): + OrderedDict([('a', 1), ('b', 2)], None) # too many args + pairs = [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)] + self.assertEqual(sorted(OrderedDict(dict(pairs)).items()), pairs) # dict input + self.assertEqual(sorted(OrderedDict(**dict(pairs)).items()), pairs) # kwds input + self.assertEqual(list(OrderedDict(pairs).items()), pairs) # pairs input + self.assertEqual(list(OrderedDict([('a', 1), ('b', 2), ('c', 9), ('d', 4)], + c=3, e=5).items()), pairs) # mixed input + + # make sure no positional args conflict with possible kwdargs + self.assertEqual(inspect.getargspec(OrderedDict.__dict__['__init__']).args, + ['self']) + + # Make sure that direct calls to __init__ do not clear previous contents + d = OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 44), ('e', 55)]) + d.__init__([('e', 5), ('f', 6)], g=7, d=4) + self.assertEqual(list(d.items()), + [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5), ('f', 6), ('g', 7)]) + + def test_update(self): + with self.assertRaises(TypeError): + OrderedDict().update([('a', 1), ('b', 2)], None) # too many args + pairs = [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)] + od = OrderedDict() + od.update(dict(pairs)) + self.assertEqual(sorted(od.items()), pairs) # dict input + od = OrderedDict() + od.update(**dict(pairs)) + self.assertEqual(sorted(od.items()), pairs) # kwds input + od = OrderedDict() + od.update(pairs) + self.assertEqual(list(od.items()), pairs) # pairs input + od = OrderedDict() + od.update([('a', 1), ('b', 2), ('c', 9), ('d', 4)], c=3, e=5) + self.assertEqual(list(od.items()), pairs) # mixed input + + # Make sure that direct calls to update do not clear previous contents + d = OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 44), ('e', 55)]) + d.update([('e', 5), ('f', 6)], g=7, d=4) + self.assertEqual(list(d.items()), + [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5), ('f', 6), ('g', 7)]) + + def test_clear(self): + self.assertEqual(len(self.od), len(self.pairs)) + self.od.clear() + self.assertEqual(len(self.od), 0) + + def test_delitem(self): + del self.od['a'] + self.assert_('a' not in self.od) + with self.assertRaises(KeyError): + del self.od['a'] + self.assertEqual(list(self.od.items()), self.pairs[1:]) + + def test_setitem(self): + od = OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]) + self.od['c'] = 10 # existing element + self.od['f'] = 20 # new element + self.assertEqual(list(self.od.items()), + [('a', 1), ('b', 2), ('c', 10), ('d', 4), ('e', 5), ('f', 20)]) + + def test_iterators(self): + self.assertEqual(list(self.od), [t[0] for t in self.pairs]) + self.assertEqual(list(self.od.keys()), [t[0] for t in self.pairs]) + self.assertEqual(list(self.od.values()), [t[1] for t in self.pairs]) + self.assertEqual(list(self.od.items()), self.pairs) + self.assertEqual(list(reversed(self.od)), + [t[0] for t in reversed(self.pairs)]) + + def test_popitem(self): + while self.pairs: + self.assertEqual(self.od.popitem(), self.pairs.pop()) + with self.assertRaises(KeyError): + self.od.popitem() + self.assertEqual(len(self.od), 0) + + def test_pop(self): + shuffle(self.pairs) + while self.pairs: + k, v = self.pairs.pop() + self.assertEqual(self.od.pop(k), v) + with self.assertRaises(KeyError): + self.od.pop('xyz') + self.assertEqual(len(self.od), 0) + self.assertEqual(self.od.pop(k, 12345), 12345) + + def test_equality(self): + # equality should not be order sensitive + self.assertEqual(self.od, OrderedDict(reversed(self.pairs))) + # interoperable with dicts + self.assertEqual(self.od, dict(self.pairs)) + self.assertEqual(dict(self.pairs), self.od) + + def test_copying(self): + # Check that ordered dicts are copyable, deepcopyable, picklable, + # and have a repr/eval round-trip + update_test = OrderedDict() + update_test.update(self.od) + for i, dup in enumerate([ + self.od.copy(), + copy.copy(self.od), + copy.deepcopy(self.od), + pickle.loads(pickle.dumps(self.od, 0)), + pickle.loads(pickle.dumps(self.od, 1)), + pickle.loads(pickle.dumps(self.od, 2)), + pickle.loads(pickle.dumps(self.od, 3)), + pickle.loads(pickle.dumps(self.od, -1)), + eval(repr(self.od)), + update_test, + OrderedDict(self.od), + ]): + msg = (i, dup, self.od) + self.assert_(dup is not self.od) + self.assertEquals(dup, self.od) + self.assertEquals(len(dup), len(self.od)) + self.assertEquals(type(dup), type(self.od)) + + def test_repr(self): + od = OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]) + self.assertEqual(repr(od), + "OrderedDict({'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5})") + self.assertEqual(eval(repr(od)), od) + self.assertEqual(repr(OrderedDict()), "OrderedDict()") + + def test_setdefault(self): + self.assertEqual(self.od.setdefault('a', 10), 1) + self.assertEqual(self.od.setdefault('x', 10), 10) + +class GeneralMappingTests(mapping_tests.BasicTestMappingProtocol): + type2test = OrderedDict + +class MyOrderedDict(dict): + pass + +class SubclassMappingTests(mapping_tests.BasicTestMappingProtocol): + type2test = MyOrderedDict + + + import doctest, collections def test_main(verbose=None): NamedTupleDocs = doctest.DocTestSuite(module=collections) test_classes = [TestNamedTuple, NamedTupleDocs, TestOneTrickPonyABCs, - TestCollectionABCs, TestCounter] + TestCollectionABCs, TestCounter, + TestOrderedDict, GeneralMappingTests, SubclassMappingTests] support.run_unittest(*test_classes) support.run_doctest(collections, verbose)