| @@ -18,10 +18,13 @@ v0.4 (unreleased): | |||||
| template now raises ValueError instead of doing nothing. | template now raises ValueError instead of doing nothing. | ||||
| - Parameters with non-integer keys can no longer be created with | - Parameters with non-integer keys can no longer be created with | ||||
| 'showkey=False', nor have the value of this attribute be set to False later. | 'showkey=False', nor have the value of this attribute be set to False later. | ||||
| - _ListProxy.destroy() has been changed to _ListProxy.detach(), and now works | |||||
| in a more useful way. | |||||
| - If something goes wrong while parsing, ParserError will now be raised. | - If something goes wrong while parsing, ParserError will now be raised. | ||||
| Previously, the parser would produce an unclear BadRoute exception or allow | Previously, the parser would produce an unclear BadRoute exception or allow | ||||
| an incorrect node tree to be build. | an incorrect node tree to be build. | ||||
| - Fixed a parser bug involving nested tags. | |||||
| - Fixed a parser bug involving nested tags, and another involving comments in | |||||
| template names. | |||||
| - Test coverage has been improved, and some minor related bugs have been fixed. | - Test coverage has been improved, and some minor related bugs have been fixed. | ||||
| - Updated and fixed some documentation. | - Updated and fixed some documentation. | ||||
| @@ -29,10 +29,13 @@ Unreleased | |||||
| - :py:class:`.Parameter`\ s with non-integer keys can no longer be created with | - :py:class:`.Parameter`\ s with non-integer keys can no longer be created with | ||||
| *showkey=False*, nor have the value of this attribute be set to *False* | *showkey=False*, nor have the value of this attribute be set to *False* | ||||
| later. | later. | ||||
| - :py:meth:`._ListProxy.destroy` has been changed to | |||||
| :py:meth:`._ListProxy.detach`, and now works in a more useful way. | |||||
| - If something goes wrong while parsing, :py:exc:`.ParserError` will now be | - If something goes wrong while parsing, :py:exc:`.ParserError` will now be | ||||
| raised. Previously, the parser would produce an unclear :py:exc:`.BadRoute` | raised. Previously, the parser would produce an unclear :py:exc:`.BadRoute` | ||||
| exception or allow an incorrect node tree to be build. | exception or allow an incorrect node tree to be build. | ||||
| - Fixed a parser bug involving nested tags. | |||||
| - Fixed a parser bug involving nested tags, and another involving comments in | |||||
| template names. | |||||
| - Test coverage has been improved, and some minor related bugs have been fixed. | - Test coverage has been improved, and some minor related bugs have been fixed. | ||||
| - Updated and fixed some documentation. | - Updated and fixed some documentation. | ||||
| @@ -41,6 +41,7 @@ def inheritdoc(method): | |||||
| method.__doc__ = getattr(list, method.__name__).__doc__ | method.__doc__ = getattr(list, method.__name__).__doc__ | ||||
| return method | return method | ||||
| class _SliceNormalizerMixIn(object): | class _SliceNormalizerMixIn(object): | ||||
| """MixIn that provides a private method to normalize slices.""" | """MixIn that provides a private method to normalize slices.""" | ||||
| @@ -83,7 +84,9 @@ class SmartList(_SliceNormalizerMixIn, list): | |||||
| The parent needs to keep a list of its children in order to update them, | The parent needs to keep a list of its children in order to update them, | ||||
| which prevents them from being garbage-collected. If you are keeping the | which prevents them from being garbage-collected. If you are keeping the | ||||
| parent around for a while but creating many children, it is advisable to | parent around for a while but creating many children, it is advisable to | ||||
| call :py:meth:`~._ListProxy.destroy` when you're finished with them. | |||||
| call :py:meth:`~._ListProxy.detach` when you're finished with them. Certain | |||||
| parent methods, like :py:meth:`reverse` and :py:meth:`sort`, will do this | |||||
| automatically. | |||||
| """ | """ | ||||
| def __init__(self, iterable=None): | def __init__(self, iterable=None): | ||||
| @@ -151,10 +154,10 @@ class SmartList(_SliceNormalizerMixIn, list): | |||||
| self.extend(other) | self.extend(other) | ||||
| return self | return self | ||||
| def _release_children(self): | |||||
| copy = list(self) | |||||
| for child in self._children: | |||||
| child._parent = copy | |||||
| def _detach_children(self): | |||||
| children = [val[0] for val in self._children.values()] | |||||
| for child in children: | |||||
| child.detach() | |||||
| @inheritdoc | @inheritdoc | ||||
| def append(self, item): | def append(self, item): | ||||
| @@ -184,13 +187,13 @@ class SmartList(_SliceNormalizerMixIn, list): | |||||
| @inheritdoc | @inheritdoc | ||||
| def reverse(self): | def reverse(self): | ||||
| self._release_children() | |||||
| self._detach_children() | |||||
| super(SmartList, self).reverse() | super(SmartList, self).reverse() | ||||
| if py3k: | if py3k: | ||||
| @inheritdoc | @inheritdoc | ||||
| def sort(self, key=None, reverse=None): | def sort(self, key=None, reverse=None): | ||||
| self._release_children() | |||||
| self._detach_children() | |||||
| kwargs = {} | kwargs = {} | ||||
| if key is not None: | if key is not None: | ||||
| kwargs["key"] = key | kwargs["key"] = key | ||||
| @@ -200,7 +203,7 @@ class SmartList(_SliceNormalizerMixIn, list): | |||||
| else: | else: | ||||
| @inheritdoc | @inheritdoc | ||||
| def sort(self, cmp=None, key=None, reverse=None): | def sort(self, cmp=None, key=None, reverse=None): | ||||
| self._release_children() | |||||
| self._detach_children() | |||||
| kwargs = {} | kwargs = {} | ||||
| if cmp is not None: | if cmp is not None: | ||||
| kwargs["cmp"] = cmp | kwargs["cmp"] = cmp | ||||
| @@ -223,6 +226,7 @@ class _ListProxy(_SliceNormalizerMixIn, list): | |||||
| super(_ListProxy, self).__init__() | super(_ListProxy, self).__init__() | ||||
| self._parent = parent | self._parent = parent | ||||
| self._sliceinfo = sliceinfo | self._sliceinfo = sliceinfo | ||||
| self._detached = False | |||||
| def __repr__(self): | def __repr__(self): | ||||
| return repr(self._render()) | return repr(self._render()) | ||||
| @@ -452,9 +456,17 @@ class _ListProxy(_SliceNormalizerMixIn, list): | |||||
| item.sort(**kwargs) | item.sort(**kwargs) | ||||
| self._parent[self._start:self._stop:self._step] = item | self._parent[self._start:self._stop:self._step] = item | ||||
| def destroy(self): | |||||
| """Make the parent forget this child. The child will no longer work.""" | |||||
| self._parent._children.pop(id(self)) | |||||
| def detach(self): | |||||
| """Detach the child so it operates like a normal list. | |||||
| This allows children to be properly garbage-collected if their parent | |||||
| is being kept around for a long time. This method has no effect if the | |||||
| child is already detached. | |||||
| """ | |||||
| if not self._detached: | |||||
| self._parent._children.pop(id(self)) | |||||
| self._parent = list(self._parent) | |||||
| self._detached = True | |||||
| del inheritdoc | del inheritdoc | ||||
| @@ -88,6 +88,10 @@ class TestSmartList(unittest.TestCase): | |||||
| self.assertEqual([0, 1, 2, 3, 4, 5, 6], list2) | self.assertEqual([0, 1, 2, 3, 4, 5, 6], list2) | ||||
| self.assertRaises(ValueError, assign, list2, 0, 5, 2, | self.assertRaises(ValueError, assign, list2, 0, 5, 2, | ||||
| [100, 102, 104, 106]) | [100, 102, 104, 106]) | ||||
| with self.assertRaises(IndexError): | |||||
| list2[7] = "foo" | |||||
| with self.assertRaises(IndexError): | |||||
| list2[-8] = "foo" | |||||
| del list2[2] | del list2[2] | ||||
| self.assertEqual([0, 1, 3, 4, 5, 6], list2) | self.assertEqual([0, 1, 3, 4, 5, 6], list2) | ||||
| @@ -271,6 +275,13 @@ class TestSmartList(unittest.TestCase): | |||||
| list3.sort(key=lambda i: i[1], reverse=True) | list3.sort(key=lambda i: i[1], reverse=True) | ||||
| self.assertEqual([("b", 8), ("a", 5), ("c", 3), ("d", 2)], list3) | self.assertEqual([("b", 8), ("a", 5), ("c", 3), ("d", 2)], list3) | ||||
| def _dispatch_test_for_children(self, meth): | |||||
| """Run a test method on various different types of children.""" | |||||
| meth(lambda L: SmartList(list(L))[:]) | |||||
| meth(lambda L: SmartList([999] + list(L))[1:]) | |||||
| meth(lambda L: SmartList(list(L) + [999])[:-1]) | |||||
| meth(lambda L: SmartList([101, 102] + list(L) + [201, 202])[2:-2]) | |||||
| def test_docs(self): | def test_docs(self): | ||||
| """make sure the methods of SmartList/_ListProxy have docstrings""" | """make sure the methods of SmartList/_ListProxy have docstrings""" | ||||
| methods = ["append", "count", "extend", "index", "insert", "pop", | methods = ["append", "count", "extend", "index", "insert", "pop", | ||||
| @@ -300,8 +311,8 @@ class TestSmartList(unittest.TestCase): | |||||
| """make sure SmartList's add/radd/iadd work""" | """make sure SmartList's add/radd/iadd work""" | ||||
| self._test_add_radd_iadd(SmartList) | self._test_add_radd_iadd(SmartList) | ||||
| def test_parent_unaffected_magics(self): | |||||
| """sanity checks against SmartList features that were not modified""" | |||||
| def test_parent_other_magics(self): | |||||
| """make sure SmartList's other magically implemented features work""" | |||||
| self._test_other_magic_methods(SmartList) | self._test_other_magic_methods(SmartList) | ||||
| def test_parent_methods(self): | def test_parent_methods(self): | ||||
| @@ -310,41 +321,29 @@ class TestSmartList(unittest.TestCase): | |||||
| def test_child_get_set_del(self): | def test_child_get_set_del(self): | ||||
| """make sure _ListProxy's getitem/setitem/delitem work""" | """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) | |||||
| self._dispatch_test_for_children(self._test_get_set_del_item) | |||||
| def test_child_add(self): | def test_child_add(self): | ||||
| """make sure _ListProxy's add/radd/iadd work""" | """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) | |||||
| self._dispatch_test_for_children(self._test_add_radd_iadd) | |||||
| def test_child_other_magics(self): | def test_child_other_magics(self): | ||||
| """make sure _ListProxy's other magically implemented features work""" | """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) | |||||
| self._dispatch_test_for_children(self._test_other_magic_methods) | |||||
| def test_child_methods(self): | def test_child_methods(self): | ||||
| """make sure _ListProxy's non-magic methods work, like append()""" | """make sure _ListProxy's non-magic methods work, like append()""" | ||||
| self._test_list_methods(lambda L: SmartList(list(L))[:]) | |||||
| self._test_list_methods(lambda L: SmartList([999] + list(L))[1:]) | |||||
| self._test_list_methods(lambda L: SmartList(list(L) + [999])[:-1]) | |||||
| builder = lambda L: SmartList([101, 102] + list(L) + [201, 202])[2:-2] | |||||
| self._test_list_methods(builder) | |||||
| self._dispatch_test_for_children(self._test_list_methods) | |||||
| def test_influence(self): | def test_influence(self): | ||||
| """make sure changes are propagated from parents to children""" | """make sure changes are propagated from parents to children""" | ||||
| parent = SmartList([0, 1, 2, 3, 4, 5]) | parent = SmartList([0, 1, 2, 3, 4, 5]) | ||||
| child1 = parent[2:] | child1 = parent[2:] | ||||
| child2 = parent[2:5] | child2 = parent[2:5] | ||||
| self.assertEqual([0, 1, 2, 3, 4, 5], parent) | |||||
| self.assertEqual([2, 3, 4, 5], child1) | |||||
| self.assertEqual([2, 3, 4], child2) | |||||
| self.assertEqual(2, len(parent._children)) | |||||
| parent.append(6) | parent.append(6) | ||||
| child1.append(7) | child1.append(7) | ||||
| @@ -390,5 +389,28 @@ class TestSmartList(unittest.TestCase): | |||||
| self.assertEqual([4, 3, 2, 1.9, 1.8, 5, 6, 7, 8, 8.1, 8.2], child1) | self.assertEqual([4, 3, 2, 1.9, 1.8, 5, 6, 7, 8, 8.1, 8.2], child1) | ||||
| self.assertEqual([4, 3, 2, 1.9, 1.8], child2) | self.assertEqual([4, 3, 2, 1.9, 1.8], child2) | ||||
| child1.detach() | |||||
| self.assertEqual([1, 4, 3, 2, 1.9, 1.8, 5, 6, 7, 8, 8.1, 8.2], parent) | |||||
| self.assertEqual([4, 3, 2, 1.9, 1.8, 5, 6, 7, 8, 8.1, 8.2], child1) | |||||
| self.assertEqual([4, 3, 2, 1.9, 1.8], child2) | |||||
| self.assertEqual(1, len(parent._children)) | |||||
| parent.remove(1.9) | |||||
| parent.remove(1.8) | |||||
| self.assertEqual([1, 4, 3, 2, 5, 6, 7, 8, 8.1, 8.2], parent) | |||||
| self.assertEqual([4, 3, 2, 1.9, 1.8, 5, 6, 7, 8, 8.1, 8.2], child1) | |||||
| self.assertEqual([4, 3, 2], child2) | |||||
| parent.reverse() | |||||
| self.assertEqual([8.2, 8.1, 8, 7, 6, 5, 2, 3, 4, 1], parent) | |||||
| self.assertEqual([4, 3, 2, 1.9, 1.8, 5, 6, 7, 8, 8.1, 8.2], child1) | |||||
| self.assertEqual([4, 3, 2], child2) | |||||
| self.assertEqual(0, len(parent._children)) | |||||
| child2.detach() | |||||
| self.assertEqual([8.2, 8.1, 8, 7, 6, 5, 2, 3, 4, 1], parent) | |||||
| self.assertEqual([4, 3, 2, 1.9, 1.8, 5, 6, 7, 8, 8.1, 8.2], child1) | |||||
| self.assertEqual([4, 3, 2], child2) | |||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||
| unittest.main(verbosity=2) | unittest.main(verbosity=2) | ||||