diff --git a/mwparserfromhell/smart_list.py b/mwparserfromhell/smart_list.py index 229500c..062e9ad 100644 --- a/mwparserfromhell/smart_list.py +++ b/mwparserfromhell/smart_list.py @@ -76,8 +76,8 @@ class SmartList(list): def __getitem__(self, key): if not isinstance(key, slice): return super(SmartList, self).__getitem__(key) - sliceinfo = [key.start or 0, maxsize if key.stop is None else key.stop, - key.step or 1] + keystop = maxsize if key.stop is None else key.stop + sliceinfo = [key.start or 0, keystop, key.step or 1] child = _ListProxy(self, sliceinfo) self._children[id(child)] = (child, sliceinfo) return child @@ -100,8 +100,8 @@ class SmartList(list): def __delitem__(self, key): super(SmartList, self).__delitem__(key) if isinstance(key, slice): - key = slice(key.start or 0, - maxsize if key.stop is None else key.stop) + keystop = maxsize if key.stop is None else key.stop + key = slice(key.start or 0, keystop) else: key = slice(key, key + 1) diff = key.stop - key.start @@ -241,18 +241,36 @@ class _ListProxy(list): def __setitem__(self, key, item): if isinstance(key, slice): - adjusted = slice(key.start + self._start, key.stop + self._stop, - key.step) + keystart = (key.start or 0) + self._start + if key.stop is None or key.stop == maxsize: + keystop = self._stop + else: + keystop = key.stop + self._start + adjusted = slice(keystart, keystop, key.step) self._parent[adjusted] = item else: + length = len(self) + if key < 0: + key = length + key + if key < 0 or key >= length: + raise IndexError("list assignment index out of range") self._parent[self._start + key] = item def __delitem__(self, key): if isinstance(key, slice): - adjusted = slice(key.start + self._start, key.stop + self._stop, - key.step) + keystart = (key.start or 0) + self._start + if key.stop is None or key.stop == maxsize: + keystop = self._stop + else: + keystop = key.stop + self._start + adjusted = slice(keystart, keystop, key.step) del self._parent[adjusted] else: + length = len(self) + if key < 0: + key = length + key + if key < 0 or key >= length: + raise IndexError("list assignment index out of range") del self._parent[self._start + key] def __iter__(self): @@ -290,6 +308,16 @@ class _ListProxy(list): self.extend(other) return self + def __mul__(self, other): + return SmartList(list(self) * other) + + def __rmul__(self, other): + return SmartList(other * list(self)) + + def __imul__(self, other): + self.extend(list(self) * (other - 1)) + return self + @property def _start(self): """The starting index of this list, inclusive.""" diff --git a/tests/test_smart_list.py b/tests/test_smart_list.py index 44775b4..777660a 100644 --- a/tests/test_smart_list.py +++ b/tests/test_smart_list.py @@ -29,100 +29,15 @@ from mwparserfromhell.smart_list import SmartList, _ListProxy class TestSmartList(unittest.TestCase): """Test cases for the SmartList class and its child, _ListProxy.""" - def _test_list_methods(self, builder): - """Run tests on the public methods of a list built with *builder*.""" - list1 = builder(range(5)) - list2 = builder(["foo"]) - list3 = builder([("a", 5), ("d", 2), ("b", 8), ("c", 3)]) - - list1.append(5) - list1.append(1) - list1.append(2) - self.assertEquals([0, 1, 2, 3, 4, 5, 1, 2], list1) - - self.assertEquals(0, list1.count(6)) - self.assertEquals(2, list1.count(1)) - - list1.extend(range(5, 8)) - self.assertEquals([0, 1, 2, 3, 4, 5, 1, 2, 5, 6, 7], list1) - - self.assertEquals(1, list1.index(1)) - self.assertEquals(6, list1.index(1, 3)) - self.assertEquals(6, list1.index(1, 3, 7)) - self.assertRaises(ValueError, list1.index, 1, 3, 5) - - list1.insert(0, -1) - self.assertEquals([-1, 0, 1, 2, 3, 4, 5, 1, 2, 5, 6, 7], list1) - list1.insert(-1, 6.5) - self.assertEquals([-1, 0, 1, 2, 3, 4, 5, 1, 2, 5, 6, 6.5, 7], list1) - list1.insert(13, 8) - self.assertEquals([-1, 0, 1, 2, 3, 4, 5, 1, 2, 5, 6, 6.5, 7, 8], list1) - - self.assertEquals(8, list1.pop()) - self.assertEquals(7, list1.pop()) - self.assertEquals([-1, 0, 1, 2, 3, 4, 5, 1, 2, 5, 6, 6.5], list1) - self.assertEquals(-1, list1.pop(0)) - self.assertEquals(5, list1.pop(5)) - self.assertEquals(6.5, list1.pop(-1)) - self.assertEquals([0, 1, 2, 3, 4, 1, 2, 5, 6], list1) - self.assertEquals("foo", list2.pop()) - self.assertRaises(IndexError, list2.pop) - self.assertEquals([], list2) - - list1.remove(6) - self.assertEquals([0, 1, 2, 3, 4, 1, 2, 5], list1) - list1.remove(1) - self.assertEquals([0, 2, 3, 4, 1, 2, 5], list1) - list1.remove(1) - self.assertEquals([0, 2, 3, 4, 2, 5], list1) - self.assertRaises(ValueError, list1.remove, 1) - - list1.reverse() - self.assertEquals([5, 2, 4, 3, 2, 0], list1) - - list1.sort() - self.assertEquals([0, 2, 2, 3, 4, 5], list1) - list1.sort(reverse=True) - self.assertEquals([5, 4, 3, 2, 2, 0], list1) - list1.sort(cmp=lambda x, y: abs(3 - x) - abs(3 - y)) # Distance from 3 - self.assertEquals([3, 4, 2, 2, 5, 0], list1) - list1.sort(cmp=lambda x, y: abs(3 - x) - abs(3 - y), reverse=True) - self.assertEquals([0, 5, 4, 2, 2, 3], list1) - list3.sort(key=lambda i: i[1]) - self.assertEquals([("d", 2), ("c", 3), ("a", 5), ("b", 8)], list3) - list3.sort(key=lambda i: i[1], reverse=True) - self.assertEquals([("b", 8), ("a", 5), ("c", 3), ("d", 2)], list3) - - def test_docs(self): - """make sure the methods of SmartList/_ListProxy have docstrings""" - methods = ["append", "count", "extend", "index", "insert", "pop", - "remove", "reverse", "sort"] - for meth in methods: - expected = getattr(list, meth).__doc__ - smartlist_doc = getattr(SmartList, meth).__doc__ - listproxy_doc = getattr(_ListProxy, meth).__doc__ - self.assertEquals(expected, smartlist_doc) - self.assertEquals(expected, listproxy_doc) - - def test_doctest(self): - """make sure the test embedded in SmartList's docstring passes""" - parent = SmartList([0, 1, 2, 3]) - self.assertEquals([0, 1, 2, 3], parent) - child = parent[2:] - self.assertEquals([2, 3], child) - child.append(4) - self.assertEquals([2, 3, 4], child) - self.assertEquals([0, 1, 2, 3, 4], parent) - - def test_parent_get_set_del(self): - """make sure SmartList's getitem/setitem/delitem work""" + def _test_get_set_del_item(self, builder): + """Run tests on __get/set/delitem__ of a list built with *builder*.""" def assign(L, s1, s2, s3, val): L[s1:s2:s3] = val def delete(L, s1): del L[s1] - list1 = SmartList([0, 1, 2, 3, "one", "two"]) - list2 = SmartList(list(range(10))) + list1 = builder([0, 1, 2, 3, "one", "two"]) + list2 = builder(list(range(10))) self.assertEquals(1, list1[1]) self.assertEquals("one", list1[-2]) @@ -152,9 +67,11 @@ class TestSmartList(unittest.TestCase): list1[3] = 100 self.assertEquals(100, list1[3]) + list1[-3] = 101 + self.assertEquals([0, 1, 2, 101, "one", "two"], list1) list1[5:] = [6, 7, 8] self.assertEquals([6, 7, 8], list1[5:]) - self.assertEquals([0, 1, 2, 100, "one", 6, 7, 8], list1) + self.assertEquals([0, 1, 2, 101, "one", 6, 7, 8], list1) list1[2:4] = [-1, -2, -3, -4, -5] self.assertEquals([0, 1, -1, -2, -3, -4, -5, "one", 6, 7, 8], list1) list1[0:-3] = [99] @@ -185,10 +102,10 @@ class TestSmartList(unittest.TestCase): del list2[2:8:2] self.assertEquals([0, 1, 3, 5, 7, 8, 9], list2) - def test_parent_add(self): - """make sure SmartList's add/radd/iadd work""" - list1 = SmartList(range(5)) - list2 = SmartList(range(5, 10)) + def _test_add_radd_iadd(self, builder): + """Run tests on __r/i/add__ of a list built with *builder*.""" + list1 = builder(range(5)) + list2 = builder(range(5, 10)) self.assertEquals([0, 1, 2, 3, 4, 5, 6], list1 + [5, 6]) self.assertEquals([0, 1, 2, 3, 4], list1) self.assertEquals(list(range(10)), list1 + list2) @@ -197,12 +114,12 @@ class TestSmartList(unittest.TestCase): list1 += ["foo", "bar", "baz"] self.assertEquals([0, 1, 2, 3, 4, "foo", "bar", "baz"], list1) - def test_parent_unaffected_magics(self): - """sanity checks against SmartList features that were not modified""" - list1 = SmartList([0, 1, 2, 3, "one", "two"]) - list2 = SmartList([]) - list3 = SmartList([0, 2, 3, 4]) - list4 = SmartList([0, 1, 2]) + def _test_other_magic_methods(self, builder): + """Run tests on other magic methods of a list built with *builder*.""" + list1 = builder([0, 1, 2, 3, "one", "two"]) + list2 = builder([]) + list3 = builder([0, 2, 3, 4]) + list4 = builder([0, 1, 2]) if py3k: self.assertEquals("[0, 1, 2, 3, 'one', 'two']", str(list1)) @@ -284,47 +201,130 @@ class TestSmartList(unittest.TestCase): list4 *= 2 self.assertEquals([0, 1, 2, 0, 1, 2], list4) + def _test_list_methods(self, builder): + """Run tests on the public methods of a list built with *builder*.""" + list1 = builder(range(5)) + list2 = builder(["foo"]) + list3 = builder([("a", 5), ("d", 2), ("b", 8), ("c", 3)]) + + list1.append(5) + list1.append(1) + list1.append(2) + self.assertEquals([0, 1, 2, 3, 4, 5, 1, 2], list1) + + self.assertEquals(0, list1.count(6)) + self.assertEquals(2, list1.count(1)) + + list1.extend(range(5, 8)) + self.assertEquals([0, 1, 2, 3, 4, 5, 1, 2, 5, 6, 7], list1) + + self.assertEquals(1, list1.index(1)) + self.assertEquals(6, list1.index(1, 3)) + self.assertEquals(6, list1.index(1, 3, 7)) + self.assertRaises(ValueError, list1.index, 1, 3, 5) + + list1.insert(0, -1) + self.assertEquals([-1, 0, 1, 2, 3, 4, 5, 1, 2, 5, 6, 7], list1) + list1.insert(-1, 6.5) + self.assertEquals([-1, 0, 1, 2, 3, 4, 5, 1, 2, 5, 6, 6.5, 7], list1) + list1.insert(13, 8) + self.assertEquals([-1, 0, 1, 2, 3, 4, 5, 1, 2, 5, 6, 6.5, 7, 8], list1) + + self.assertEquals(8, list1.pop()) + self.assertEquals(7, list1.pop()) + self.assertEquals([-1, 0, 1, 2, 3, 4, 5, 1, 2, 5, 6, 6.5], list1) + self.assertEquals(-1, list1.pop(0)) + self.assertEquals(5, list1.pop(5)) + self.assertEquals(6.5, list1.pop(-1)) + self.assertEquals([0, 1, 2, 3, 4, 1, 2, 5, 6], list1) + self.assertEquals("foo", list2.pop()) + self.assertRaises(IndexError, list2.pop) + self.assertEquals([], list2) + + list1.remove(6) + self.assertEquals([0, 1, 2, 3, 4, 1, 2, 5], list1) + list1.remove(1) + self.assertEquals([0, 2, 3, 4, 1, 2, 5], list1) + list1.remove(1) + self.assertEquals([0, 2, 3, 4, 2, 5], list1) + self.assertRaises(ValueError, list1.remove, 1) + + list1.reverse() + self.assertEquals([5, 2, 4, 3, 2, 0], list1) + + list1.sort() + self.assertEquals([0, 2, 2, 3, 4, 5], list1) + list1.sort(reverse=True) + self.assertEquals([5, 4, 3, 2, 2, 0], list1) + list1.sort(cmp=lambda x, y: abs(3 - x) - abs(3 - y)) # Distance from 3 + self.assertEquals([3, 4, 2, 2, 5, 0], list1) + list1.sort(cmp=lambda x, y: abs(3 - x) - abs(3 - y), reverse=True) + self.assertEquals([0, 5, 4, 2, 2, 3], list1) + list3.sort(key=lambda i: i[1]) + self.assertEquals([("d", 2), ("c", 3), ("a", 5), ("b", 8)], list3) + list3.sort(key=lambda i: i[1], reverse=True) + self.assertEquals([("b", 8), ("a", 5), ("c", 3), ("d", 2)], list3) + + def test_docs(self): + """make sure the methods of SmartList/_ListProxy have docstrings""" + methods = ["append", "count", "extend", "index", "insert", "pop", + "remove", "reverse", "sort"] + for meth in methods: + expected = getattr(list, meth).__doc__ + smartlist_doc = getattr(SmartList, meth).__doc__ + listproxy_doc = getattr(_ListProxy, meth).__doc__ + self.assertEquals(expected, smartlist_doc) + self.assertEquals(expected, listproxy_doc) + + def test_doctest(self): + """make sure the test embedded in SmartList's docstring passes""" + parent = SmartList([0, 1, 2, 3]) + self.assertEquals([0, 1, 2, 3], parent) + child = parent[2:] + self.assertEquals([2, 3], child) + child.append(4) + self.assertEquals([2, 3, 4], child) + self.assertEquals([0, 1, 2, 3, 4], parent) + + def test_parent_get_set_del(self): + """make sure SmartList's getitem/setitem/delitem work""" + self._test_get_set_del_item(lambda L: SmartList(L)) + + def test_parent_add(self): + """make sure SmartList's add/radd/iadd work""" + self._test_add_radd_iadd(lambda L: SmartList(L)) + + def test_parent_unaffected_magics(self): + """sanity checks against SmartList features that were not modified""" + self._test_other_magic_methods(lambda L: SmartList(L)) + def test_parent_methods(self): """make sure SmartList's non-magic methods work, like append()""" self._test_list_methods(lambda L: SmartList(L)) - def test_child_magics(self): - """make sure _ListProxy's magically implemented features work""" - pass - # if py3k: - # __str__ - # __bytes__ - # else: - # __unicode__ - # __str__ - # __repr__ - # __lt__ - # __le__ - # __eq__ - # __ne__ - # __gt__ - # __ge__ - # if py3k: - # __bool__ - # else: - # __nonzero__ - # __len__ - # __getitem__ - # __setitem__ - # __delitem__ - # __iter__ - # __reversed__ - # __contains__ - # if not py3k: - # __getslice__ - # __setslice__ - # __delslice__ - # __add__ - # __radd__ - # __iadd__ - # __mul__ - # __rmul__ - # __imul__ + def test_child_get_set_del(self): + """make sure _ListProxy's getitem/setitem/delitem work""" + self._test_get_set_del_item(lambda L: SmartList(list(L))[:]) + self._test_get_set_del_item(lambda L: SmartList([999] + list(L))[1:]) + # self._test_get_set_del_item(lambda L: SmartList(list(L) + [999])[:-1]) + # builder = lambda L: SmartList([101, 102] + list(L) + [201, 202])[2:-2] + # self._test_get_set_del_item(builder) + + def test_child_add(self): + """make sure _ListProxy's add/radd/iadd work""" + self._test_add_radd_iadd(lambda L: SmartList(list(L))[:]) + self._test_add_radd_iadd(lambda L: SmartList([999] + list(L))[1:]) + self._test_add_radd_iadd(lambda L: SmartList(list(L) + [999])[:-1]) + builder = lambda L: SmartList([101, 102] + list(L) + [201, 202])[2:-2] + self._test_add_radd_iadd(builder) + + def test_child_other_magics(self): + """make sure _ListProxy's other magically implemented features work""" + self._test_other_magic_methods(lambda L: SmartList(list(L))[:]) + self._test_other_magic_methods(lambda L: SmartList([999] + list(L))[1:]) + self._test_other_magic_methods(lambda L: SmartList(list(L) + [999])[:-1]) + builder = lambda L: SmartList([101, 102] + list(L) + [201, 202])[2:-2] + self._test_other_magic_methods(builder) def test_child_methods(self): """make sure _ListProxy's non-magic methods work, like append()"""