diff --git a/CHANGELOG b/CHANGELOG index 629ab82..51a3c5a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,8 @@ v0.4.1 (unreleased): includes when denoting tags, but not comments. - Fixed the behavior of preserve_spacing in Template.add() and keep_field in Template.remove() on parameters with hidden keys. +- Removed _ListProxy.detach(). SmartLists now use weak references and their + children are garbage-collected properly. - Fixed parser bugs involving: - templates with completely blank names; - templates with newlines and comments. diff --git a/docs/changelog.rst b/docs/changelog.rst index 05fc1b8..39b9ab3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,8 +13,10 @@ Unreleased - Added support for Python 3.5. - ``<`` and ``>`` are now disallowed in wikilink titles and template names. This includes when denoting tags, but not comments. -- Fixed the behavior of *preserve_spacing* in :func:`~.Template.add` and - *keep_field* in :func:`~.Template.remove` on parameters with hidden keys. +- Fixed the behavior of *preserve_spacing* in :meth:`.Template.add` and + *keep_field* in :meth:`.Template.remove` on parameters with hidden keys. +- Removed :meth:`._ListProxy.detach`. :class:`.SmartList`\ s now use weak + references and their children are garbage-collected properly. - Fixed parser bugs involving: - templates with completely blank names; diff --git a/mwparserfromhell/smart_list.py b/mwparserfromhell/smart_list.py index c552050..a0f858f 100644 --- a/mwparserfromhell/smart_list.py +++ b/mwparserfromhell/smart_list.py @@ -27,6 +27,7 @@ reflect changes made to the main list, and vice-versa. """ from __future__ import unicode_literals +from weakref import ref from .compat import maxsize, py3k @@ -80,13 +81,6 @@ class SmartList(_SliceNormalizerMixIn, list): [2, 3, 4] >>> parent [0, 1, 2, 3, 4] - - 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 - parent around for a while but creating many children, it is advisable to - call :meth:`._ListProxy.detach` when you're finished with them. Certain - parent methods, like :meth:`reverse` and :meth:`sort`, will do this - automatically. """ def __init__(self, iterable=None): @@ -102,7 +96,8 @@ class SmartList(_SliceNormalizerMixIn, list): key = self._normalize_slice(key) sliceinfo = [key.start, key.stop, key.step] child = _ListProxy(self, sliceinfo) - self._children[id(child)] = (child, sliceinfo) + child_ref = ref(child, self._delete_child) + self._children[id(child_ref)] = (child_ref, sliceinfo) return child def __setitem__(self, key, item): @@ -112,13 +107,14 @@ class SmartList(_SliceNormalizerMixIn, list): super(SmartList, self).__setitem__(key, item) key = self._normalize_slice(key) diff = len(item) + (key.start - key.stop) // key.step + if not diff: + return values = self._children.values if py3k else self._children.itervalues - if diff: - for child, (start, stop, step) in values(): - if start > key.stop: - self._children[id(child)][1][0] += diff - if stop >= key.stop and stop != maxsize: - self._children[id(child)][1][1] += diff + for child, (start, stop, step) in values(): + if start > key.stop: + self._children[id(child)][1][0] += diff + if stop >= key.stop and stop != maxsize: + self._children[id(child)][1][1] += diff def __delitem__(self, key): super(SmartList, self).__delitem__(key) @@ -154,10 +150,16 @@ class SmartList(_SliceNormalizerMixIn, list): self.extend(other) return self + def _delete_child(self, child_ref): + """Remove a child reference that is about to be garbage-collected.""" + del self._children[id(child_ref)] + def _detach_children(self): + """Remove all children and give them independent parent copies.""" children = [val[0] for val in self._children.values()] for child in children: - child.detach() + child()._parent = list(self) + self._children.clear() @inheritdoc def append(self, item): @@ -226,7 +228,6 @@ class _ListProxy(_SliceNormalizerMixIn, list): super(_ListProxy, self).__init__() self._parent = parent self._sliceinfo = sliceinfo - self._detached = False def __repr__(self): return repr(self._render()) @@ -456,17 +457,5 @@ class _ListProxy(_SliceNormalizerMixIn, list): item.sort(**kwargs) self._parent[self._start:self._stop:self._step] = item - 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 diff --git a/tests/test_smart_list.py b/tests/test_smart_list.py index a7106e4..794bfc8 100644 --- a/tests/test_smart_list.py +++ b/tests/test_smart_list.py @@ -389,28 +389,20 @@ 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], child2) - child1.detach() + del child1 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__": unittest.main(verbosity=2)