# HG changeset patch
# User Christian Boos <cboos@neuf.fr>
# Date 1204545530 -3600
# Node ID 43f0a2eaea926f7db4f9b77e70560bb66e887330
# Parent  0bf651204a8abeb4f5c875b715e7b7976c50af89
Workaround a memory leak happening when an exception is raised while rendering a template.

More specifically, when an exception is raised while evaluating Python code located in expressions or code blocks inside a template, there are some situations where the `data` dictionary placed in the `globals()` used for the evaluation will be kept alive. This will have particularly severe consequences if that data dictionary itself references lots of objects, which is not uncommon.

The workaround consists to clear that leaked `data` (a `genshi.template.base.Context` object) so that at least the objects referenced by the Context won't be leaked (the Context itself will still be leaked, but it's far less critical).
For clearing the Context, we need to clear all the three of `frames`, `pop` and `push` attributes. Note that the problem happens whether `frames` is a real `deque` or the list based one, and the fix is the same in both cases as well.

Fixes #190.

diff --git a/genshi/template/base.py b/genshi/template/base.py
--- a/genshi/template/base.py
+++ b/genshi/template/base.py
@@ -150,6 +150,11 @@
             return self.get(name, default)
         data.setdefault('defined', defined)
         data.setdefault('value_of', value_of)
+
+    def clear(self):
+        del self.frames
+        del self.pop
+        del self.push
 
     def __repr__(self):
         return repr(list(self.frames))
diff --git a/genshi/template/eval.py b/genshi/template/eval.py
--- a/genshi/template/eval.py
+++ b/genshi/template/eval.py
@@ -141,7 +141,11 @@
         __traceback_hide__ = 'before_and_this'
         _globals = self._globals()
         _globals['__data__'] = data
-        return eval(self.code, _globals, {'__data__': data})
+        try:
+            return eval(self.code, _globals, {'__data__': data})
+        except:
+            data.clear()
+            raise
 
 
 class Suite(Code):
@@ -163,7 +167,11 @@
         __traceback_hide__ = 'before_and_this'
         _globals = self._globals()
         _globals['__data__'] = data
-        exec self.code in _globals, data
+        try:
+            exec self.code in _globals, data
+        except:
+            data.clear()
+            raise
 
 
 UNDEFINED = object()
diff --git a/genshi/template/tests/markup.py b/genshi/template/tests/markup.py
--- a/genshi/template/tests/markup.py
+++ b/genshi/template/tests/markup.py
@@ -22,6 +22,7 @@
 from genshi.core import Markup
 from genshi.input import XML
 from genshi.template.base import BadDirectiveError, TemplateSyntaxError
+from genshi.template.eval import UndefinedError
 from genshi.template.loader import TemplateLoader, TemplateNotFound
 from genshi.template.markup import MarkupTemplate
 
@@ -109,6 +110,39 @@
             self.assertEqual('test.html', e.filename)
             if sys.version_info[:2] >= (2, 4):
                 self.assertEqual(3, e.lineno)
+
+    def test_eval_leak_on_exception(self):
+        try:
+            from itertools import groupby
+        except ImportError:
+            return # we need groupby to exhibit the problem
+        xml = MarkupTemplate("""
+        <html xmlns:py="http://genshi.edgewall.org/">
+          <body>
+            <py:for each="i, j in groupby(test, key=lambda x: len(x))">
+              ${say.hello} (this expression will raise an UndefinedError)
+            </py:for>
+          </body>
+        </html>
+        """, lookup='lenient')
+        def strlen(x): # Note: when using `strlen` instead of the lambda in the
+            len(x)     #       `groupby` in the template above, there's no leak
+        test = dict.fromkeys(map(str, range(1000, 100000)))
+        # snapshot the current memory usage
+        import gc
+        i = 2
+        while i:
+            gc.collect()
+            num_objects = len(gc.get_objects())
+            try:
+                xml.generate(test=test, groupby=groupby, strlen=strlen).render()
+            except UndefinedError:
+                pass
+            i -= 1 # 2 passes needed for reaching fix-point
+        # check the memory usage again
+        gc.collect()
+        self.assertEqual(num_objects + 1, len(gc.get_objects()))
+        # Note: the + 1 accounts for the list created by gc.get_objects above
 
     def test_markup_noescape(self):
         """
