Repository: jazzband/django-debug-toolbar

Issue:
Infinite recursion for template loaders that query the database
A template loader that does a database query can cause the debug toolbar to enter an infinite recursive loop. (Eventually hits max recursion depth.) E.g.

```
def load_template_source(template_name, template_dirs=None):
    return DatabaseTemplate.objects.get(filename=template_name).source
```

Not an issue with an earlier version (0.7.0) of the toolbar.

Disabling the SQL panel also addresses the problem.



Discussion:
Can you provide the stack trace until the point where it enters a loop?

Here's a gist with the stack trace: https://gist.github.com/tcarobruce/219c24cf288b3ccbe550

The template loader executes a query which is picked up by `_record` in `panels.sql.tracking.NormalCursorWrapper`, which issues a call to `get_template_info` (which begins the recursion anew).


Resolving patch:
diff --git a/debug_toolbar/panels/cache.py b/debug_toolbar/panels/cache.py
--- a/debug_toolbar/panels/cache.py
+++ b/debug_toolbar/panels/cache.py
@@ -9,7 +9,6 @@
 from django.core.cache import cache as original_cache, get_cache as original_get_cache
 from django.core.cache.backends.base import BaseCache
 from django.dispatch import Signal
-from django.template import Node
 from django.utils.translation import ugettext_lazy as _, ungettext
 try:
     from collections import OrderedDict
@@ -37,19 +36,7 @@ def wrapped(self, *args, **kwargs):
         else:
             stacktrace = []
 
-        template_info = None
-        cur_frame = sys._getframe().f_back
-        try:
-            while cur_frame is not None:
-                if cur_frame.f_code.co_name == 'render':
-                    node = cur_frame.f_locals['self']
-                    if isinstance(node, Node):
-                        template_info = get_template_info(node.source)
-                        break
-                cur_frame = cur_frame.f_back
-        except Exception:
-            pass
-        del cur_frame
+        template_info = get_template_info()
         cache_called.send(sender=self.__class__, time_taken=t,
                           name=method.__name__, return_value=value,
                           args=args, kwargs=kwargs, trace=stacktrace,
diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py
--- a/debug_toolbar/panels/sql/tracking.py
+++ b/debug_toolbar/panels/sql/tracking.py
@@ -1,12 +1,9 @@
 from __future__ import absolute_import, unicode_literals
 
-import sys
-
 import json
 from threading import local
 from time import time
 
-from django.template import Node
 from django.utils.encoding import force_text
 from django.utils import six
 
@@ -115,19 +112,7 @@ def _record(self, method, sql, params):
             except Exception:
                 pass  # object not JSON serializable
 
-            template_info = None
-            cur_frame = sys._getframe().f_back
-            try:
-                while cur_frame is not None:
-                    if cur_frame.f_code.co_name == 'render':
-                        node = cur_frame.f_locals['self']
-                        if isinstance(node, Node):
-                            template_info = get_template_info(node.source)
-                            break
-                    cur_frame = cur_frame.f_back
-            except Exception:
-                pass
-            del cur_frame
+            template_info = get_template_info()
 
             alias = getattr(self.db, 'alias', 'default')
             conn = self.db.connection
diff --git a/debug_toolbar/utils.py b/debug_toolbar/utils.py
--- a/debug_toolbar/utils.py
+++ b/debug_toolbar/utils.py
@@ -15,6 +15,7 @@
 
 import django
 from django.core.exceptions import ImproperlyConfigured
+from django.template import Node
 from django.utils.encoding import force_text
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
@@ -86,7 +87,34 @@ def render_stacktrace(trace):
     return mark_safe('\n'.join(stacktrace))
 
 
-def get_template_info(source, context_lines=3):
+def get_template_info():
+    template_info = None
+    cur_frame = sys._getframe().f_back
+    try:
+        while cur_frame is not None:
+            in_utils_module = cur_frame.f_code.co_filename.endswith(
+                "/debug_toolbar/utils.py"
+            )
+            is_get_template_context = (
+                cur_frame.f_code.co_name == get_template_context.__name__
+            )
+            if in_utils_module and is_get_template_context:
+                # If the method in the stack trace is this one
+                # then break from the loop as it's being check recursively.
+                break
+            elif cur_frame.f_code.co_name == 'render':
+                node = cur_frame.f_locals['self']
+                if isinstance(node, Node):
+                    template_info = get_template_context(node.source)
+                    break
+            cur_frame = cur_frame.f_back
+    except Exception:
+        pass
+    del cur_frame
+    return template_info
+
+
+def get_template_context(source, context_lines=3):
     line = 0
     upto = 0
     source_lines = []


Test patch:
diff --git a/tests/loaders.py b/tests/loaders.py
new file mode 100644
--- /dev/null
+++ b/tests/loaders.py
@@ -0,0 +1,10 @@
+from django.contrib.auth.models import User
+from django.template.loaders.app_directories import Loader
+
+
+class LoaderWithSQL(Loader):
+    def load_template_source(self, template_name, template_dirs=None):
+        # Force the template loader to run some SQL. Simulates a CMS.
+        User.objects.all().count()
+        return super(LoaderWithSQL, self).load_template_source(
+            template_name, template_dirs=template_dirs)
diff --git a/tests/panels/test_sql.py b/tests/panels/test_sql.py
--- a/tests/panels/test_sql.py
+++ b/tests/panels/test_sql.py
@@ -2,10 +2,13 @@
 
 from __future__ import absolute_import, unicode_literals
 
+import django
 from django.contrib.auth.models import User
 from django.db import connection
 from django.db.utils import DatabaseError
+from django.shortcuts import render
 from django.utils import unittest
+from django.test.utils import override_settings
 
 from ..base import BaseTestCase
 
@@ -84,3 +87,30 @@ def test_disable_stacktraces(self):
 
         # ensure the stacktrace is empty
         self.assertEqual([], query[1]['stacktrace'])
+
+    @unittest.skipIf(django.VERSION < (1, 5),
+                     "Django 1.4 loads the TEMPLATE_LOADERS before "
+                     "override_settings can modify the settings.")
+    @override_settings(DEBUG=True, TEMPLATE_DEBUG=True,
+                       TEMPLATE_LOADERS=('tests.loaders.LoaderWithSQL',))
+    def test_regression_infinite_recursion(self):
+        """
+        Test case for when the template loader runs a SQL query that causes
+        an infinite recursion in the SQL panel.
+        """
+        self.assertEqual(len(self.panel._queries), 0)
+
+        render(self.request, "basic.html", {})
+
+        # ensure queries were logged
+        # It's more than one because the SQL run in the loader is run every time
+        # the template is rendered which is more than once.
+        self.assertEqual(len(self.panel._queries), 3)
+        query = self.panel._queries[0]
+        self.assertEqual(query[0], 'default')
+        self.assertTrue('sql' in query[1])
+        self.assertTrue('duration' in query[1])
+        self.assertTrue('stacktrace' in query[1])
+
+        # ensure the stacktrace is populated
+        self.assertTrue(len(query[1]['stacktrace']) > 0)
diff --git a/tests/templates/base.html b/tests/templates/base.html
new file mode 100644
--- /dev/null
+++ b/tests/templates/base.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>{{ title }}</title>
+    </head>
+    <body>
+    {% block content %}{% endblock %}
+    </body>
+</html>
diff --git a/tests/templates/basic.html b/tests/templates/basic.html
--- a/tests/templates/basic.html
+++ b/tests/templates/basic.html
@@ -1,8 +1,2 @@
-<!DOCTYPE html>
-<html>
-    <head>
-        <title>{{ title }}</title>
-    </head>
-    <body>
-    </body>
-</html>
+{% extends "base.html" %}
+{% block content %}Test for {{ title }}{% endblock %}