Repository: microsoft/ptvsd

Issue:
Debugger crashes when using Django's Development Server with a Proper Launch Profile
@pymike00 commented on [Thu Mar 28 2019](https://github.com/Microsoft/vscode-python/issues/4988)

## Environment data

- VS Code version: 1.32.2-1
- Extension version: 2019.3.6139
- OS and version: Linux Manjaro XFCE x86_64 18.0.4
- Python version: 3.7.2
- Type of virtual environment used: venv 
- Relevant/affected Python packages and their versions: Django 2.1.7

## Expected behaviour

Using the Python Extension on Visual Studio Code to run Django's development server as explained in the docs, [https://code.visualstudio.com/docs/python/tutorial-django#_create-a-debugger-launch-profile](https://code.visualstudio.com/docs/python/tutorial-django#_create-a-debugger-launch-profile), without any problems.

## Actual behaviour

Can't use the debugger to launch Django's development server; As soon as you try to use the site, the debugger pauses on the exception VariableDoesNotExist, even on a fresh Django Project.

The problem does not manifest itself when using a previous version of the Python Extension (such as 2019.2.5558), nor it manifests itself when running Django's development server from a shell, or even from VS Code's Terminal.

## Steps to reproduce:

1. Create a Virtual Environment with VENV and install Django.
2. Create New Project: django-admin startproject NewProject
3. Enter Folder: cd NewProject 
3. Open The Editor: code .
4. Select Python Interpreter to be the one of the previously created VENV
5. Create debugger Launch profile as explained in the docs. Comment out "no threading"
6. Launch dev Server with the newly created Launch profile (terminal will show no problems)
7. Open Chrome, go to localhost:8000. Hello World Page will load, but VS Code will show "Could not load source '<unknown source>': Source unavailable.". Debugger paused on exception <unknown source>
8. Go to  localhost:8000/admin/. Debugger will pause on exception VariableDoesNotExist as shown below.

## Logs
Impossible to get the output as explained from the Output panel in Python: it is empty and the development server doesn't show any problem. **Everything works fine if running Django's development server from a shell or from the terminal**. The log below is the message shown in red (as shown in the screenshot) with the VariableDoesNotExist exception.

```
Exception has occurred: VariableDoesNotExist
Failed lookup for key [is_popup] in [{'True': True, 'False': False, 'None': None}, {'csrf_token': <SimpleLazyObject: <function csrf.<locals>._get_val at 0x7fe8884261e0>>, 'request': <WSGIRequest: GET '/admin/login/?next=/admin/'>, 'user': <SimpleLazyObject: <django.contrib.auth.models.AnonymousUser object at 0x7fe8884740b8>>, 'perms': <django.contrib.auth.context_processors.PermWrapper object at 0x7fe88839c7b8>, 'messages': <django.contrib.messages.storage.fallback.FallbackStorage object at 0x7fe8884747f0>, 'DEFAULT_MESSAGE_LEVELS': {'DEBUG': 10, 'INFO': 20, 'SUCCESS': 25, 'WARNING': 30, 'ERROR': 40}}, {}, {'form': <AdminAuthenticationForm bound=False, valid=Unknown, fields=(username;password)>, 'view': <django.contrib.auth.views.LoginView object at 0x7fe8883da9b0>, 'site_title': 'Django site admin', 'site_header': 'Django administration', 'site_url': '/', 'has_permission': False, 'available_apps': [], 'title': 'Log in', 'app_path': '/admin/login/?next=/admin/', 'username': '', 'next': '/admin/', 'site': <django.contrib.sites.requests.RequestSite object at 0x7fe888474668>, 'site_name': '127.0.0.1:8000', 'LANGUAGE_CODE': 'en-us', 'LANGUAGE_BIDI': False}]
  File "/home/pymike00/Desktop/TestWithFreshProject/venv/lib/python3.7/site-packages/django/template/base.py", line 850, in _resolve_lookup
    (bit, current))  # missing attribute
  File "/home/pymike00/Desktop/TestWithFreshProject/venv/lib/python3.7/site-packages/django/template/base.py", line 796, in resolve
    value = self._resolve_lookup(context)
  File "/home/pymike00/Desktop/TestWithFreshProject/venv/lib/python3.7/site-packages/django/template/base.py", line 671, in resolve
    obj = self.var.resolve(context)
  File "/home/pymike00/Desktop/TestWithFreshProject/venv/lib/python3.7/site-packages/django/template/defaulttags.py", line 875, in eval
    return self.value.resolve(context, ignore_failures=True)
  File "/home/pymike00/Desktop/TestWithFreshProject/venv/lib/python3.7/site-packages/django/template/defaulttags.py", line 302, in render
    match = condition.eval(context)
  File "/home/pymike00/Desktop/TestWithFreshProject/venv/lib/python3.7/site-packages/django/template/base.py", line 904, in render_annotated
    return self.render(context)
  File "/home/pymike00/Desktop/TestWithFreshProject/venv/lib/python3.7/site-packages/django/template/base.py", line 937, in render
    bit = node.render_annotated(context)
  File "/home/pymike00/Desktop/TestWithFreshProject/venv/lib/python3.7/site
```   

Output from `Console` under the `Developer Tools` panel (toggle Developer Tools on under `Help`; turn on source maps to make any tracebacks be useful by running `Enable source map support for extension debugging`)

```
[Extension Host] Python Extension: Cached data exists getEnvironmentVariables, <unknown source>
console.ts:134 [Extension Host] Python Extension: Cached data exists getEnvironmentVariables, /home/pymike00/Desktop/TestWithFreshProject/venv/lib/python3.7/site-packages/django/contrib/admin/templates/admin/base.html
```

The following is a screenshot of everything explained above. Chrome just hangs. No page or error is shown. Thanks for your precious work.


![screen](https://user-images.githubusercontent.com/32687496/55156153-7c3fb580-5159-11e9-8bfb-289995723fb6.png)

---

@ohlr commented on [Thu Mar 28 2019](https://github.com/Microsoft/vscode-python/issues/4988#issuecomment-477601737)

I can confirm this bug:
```
Version: 1.32.3
Commit: a3db5be9b5c6ba46bb7555ec5d60178ecc2eaae4
Date: 2019-03-14T23:38:49.842Z
Electron: 3.1.6
Chrome: 66.0.3359.181
Node.js: 10.2.0
V8: 6.6.346.32
OS: Linux x64 4.15.0-46-generic
```


As quick fix use the following launch.json

```
        {
            "name": "Python: Django",
            "type": "python",
            "request": "launch",
            "program": "${workspaceFolder}/my_project/manage.py",
            "console": "integratedTerminal",
            "args": [
                "runserver",
                "--noreload",
                "--nothreading",
            ],
            "django": false <- **if true template rendering does not work correctly**
        },
```

---

@pymike00 commented on [Thu Mar 28 2019](https://github.com/Microsoft/vscode-python/issues/4988#issuecomment-477686141)

This works on my setup as well, with "--nothreading" commented out (I have always been commenting out "--nothreading" as suggested by VSCode docs, considering that I normally develop on Chromium). 
Thanks for sharing!

> ```
> As quick fix use the following launch.json
> 
> ```
>         {
>             "name": "Python: Django",
>             "type": "python",
>             "request": "launch",
>             "program": "${workspaceFolder}/my_project/manage.py",
>             "console": "integratedTerminal",
>             "args": [
>                 "runserver",
>                 "--noreload",
>                 "--nothreading",
>             ],
>             "django": false <- **if true template rendering does not work correctly**
>         },
> ```






Discussion:

Resolving patch:
diff --git a/src/ptvsd/_vendored/pydevd/pydevd_plugins/django_debug.py b/src/ptvsd/_vendored/pydevd/pydevd_plugins/django_debug.py
--- a/src/ptvsd/_vendored/pydevd/pydevd_plugins/django_debug.py
+++ b/src/ptvsd/_vendored/pydevd/pydevd_plugins/django_debug.py
@@ -358,6 +358,17 @@ def _is_django_variable_does_not_exist_exception_break_context(frame):
         name = None
     return name in ('_resolve_lookup', 'find_template')
 
+
+def _is_ignoring_failures(frame):
+    while frame is not None:
+        if frame.f_code.co_name == 'resolve':
+            ignore_failures = frame.f_locals.get('ignore_failures')
+            if ignore_failures:
+                return True
+        frame = frame.f_back
+
+    return False
+
 #=======================================================================================================================
 # Django Step Commands
 #=======================================================================================================================
@@ -466,6 +477,21 @@ def suspend(plugin, main_debugger, thread, frame, bp_type):
     return None
 
 
+def _get_filename_from_origin_in_parent_frame_locals(frame, parent_frame_name):
+    filename = None
+    parent_frame = frame
+    while parent_frame.f_code.co_name != parent_frame_name:
+        parent_frame = parent_frame.f_back
+
+    origin = None
+    if parent_frame is not None:
+        origin = parent_frame.f_locals.get('origin')
+
+    if hasattr(origin, 'name') and origin.name is not None:
+        filename = normcase(_convert_to_str(origin.name))
+    return filename
+
+
 def exception_break(plugin, main_debugger, pydb_frame, frame, args, arg):
     main_debugger = args[0]
     thread = args[3]
@@ -479,19 +505,21 @@ def exception_break(plugin, main_debugger, pydb_frame, frame, args, arg):
                 # In this case we don't actually have a regular render frame with the context
                 # (we didn't really get to that point).
                 token = getattr(value, 'token', None)
+
+                if token is None:
+                    # Django 1.7 does not have token in exception. Try to get it from locals.
+                    token = frame.f_locals.get('token')
+
                 lineno = getattr(token, 'lineno', None)
+
                 filename = None
                 if lineno is not None:
-                    get_template_frame = frame
-                    while get_template_frame.f_code.co_name != 'get_template':
-                        get_template_frame = get_template_frame.f_back
-
-                    origin = None
-                    if get_template_frame is not None:
-                        origin = get_template_frame.f_locals.get('origin')
+                    filename = _get_filename_from_origin_in_parent_frame_locals(frame, 'get_template')
 
-                    if hasattr(origin, 'name') and origin.name is not None:
-                        filename = normcase(_convert_to_str(origin.name))
+                    if filename is None:
+                        # Django 1.7 does not have origin in get_template. Try to get it from
+                        # load_template.
+                        filename = _get_filename_from_origin_in_parent_frame_locals(frame, 'load_template')
 
                 if filename is not None and lineno is not None:
                     syntax_error_frame = DjangoTemplateSyntaxErrorFrame(
@@ -503,15 +531,16 @@ def exception_break(plugin, main_debugger, pydb_frame, frame, args, arg):
 
             elif exception.__name__ == 'VariableDoesNotExist':
                 if _is_django_variable_does_not_exist_exception_break_context(frame):
-                    render_frame = _find_django_render_frame(frame)
-                    if render_frame:
-                        suspend_frame = suspend_django(
-                            main_debugger, thread, DjangoTemplateFrame(render_frame), CMD_ADD_EXCEPTION_BREAK)
-                        if suspend_frame:
-                            add_exception_to_frame(suspend_frame, (exception, value, trace))
-                            thread.additional_info.pydev_message = 'VariableDoesNotExist'
-                            suspend_frame.f_back = frame
-                            frame = suspend_frame
-                            return True, frame
+                    if not getattr(exception, 'silent_variable_failure', False) and not _is_ignoring_failures(frame):
+                        render_frame = _find_django_render_frame(frame)
+                        if render_frame:
+                            suspend_frame = suspend_django(
+                                main_debugger, thread, DjangoTemplateFrame(render_frame), CMD_ADD_EXCEPTION_BREAK)
+                            if suspend_frame:
+                                add_exception_to_frame(suspend_frame, (exception, value, trace))
+                                thread.additional_info.pydev_message = 'VariableDoesNotExist'
+                                suspend_frame.f_back = frame
+                                frame = suspend_frame
+                                return True, frame
 
     return None


Test patch:
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/debugger_fixtures.py b/src/ptvsd/_vendored/pydevd/tests_python/debugger_fixtures.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/debugger_fixtures.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/debugger_fixtures.py
@@ -10,6 +10,7 @@
     CMD_ADD_EXCEPTION_BREAK
 
 import sys
+import time
 
 
 def get_java_location():
@@ -134,6 +135,15 @@ def create_request_thread(self, url=''):
 
         class T(threading.Thread):
 
+            def wait_for_contents(self):
+                for _ in range(10):
+                    if hasattr(self, 'contents'):
+                        break
+                    time.sleep(.3)
+                else:
+                    raise AssertionError('Django did not return contents properly!')
+                return self.contents
+
             def run(self):
                 try:
                     from urllib.request import urlopen
@@ -203,6 +213,15 @@ def create_request_thread(self, uri):
 
         class T(threading.Thread):
 
+            def wait_for_contents(self):
+                for _ in range(10):
+                    if hasattr(self, 'contents'):
+                        break
+                    time.sleep(.3)
+                else:
+                    raise AssertionError('Django did not return contents properly!')
+                return self.contents
+
             def run(self):
                 try:
                     from urllib.request import urlopen
@@ -442,10 +461,10 @@ def test_file(self, **kwargs):
             version = [int(x) for x in django.get_version().split('.')][:2]
             if version == [1, 7]:
                 django_folder = 'my_django_proj_17'
-            elif version == [2, 1]:
+            elif version in ([2, 1], [2, 2]):
                 django_folder = 'my_django_proj_21'
             else:
-                raise AssertionError('Can only check django 1.7 and 2.1 right now. Found: %s' % (version,))
+                raise AssertionError('Can only check django 1.7, 2.1 and 2.2 right now. Found: %s' % (version,))
 
             WriterThread.DJANGO_FOLDER = django_folder
             for key, value in kwargs.items():
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/inherited.html b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/inherited.html
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/inherited.html
@@ -0,0 +1,5 @@
+{% if chat_mode %}
+    "chat_mode=True"
+{% else %}
+    "chat_mode=False"
+{% endif %}
\ No newline at end of file
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/inherits.html b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/inherits.html
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/inherits.html
@@ -0,0 +1,2 @@
+{% include 'my_app/inherited.html' with chat_mode=True %}
+{% include 'my_app/inherited.html' with chat_mode=False %}
\ No newline at end of file
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/no_var_error.html b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/no_var_error.html
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/no_var_error.html
@@ -0,0 +1,5 @@
+{% if pat.name %}
+    pat.name={{ pat.name }}
+{% else %}
+    no_pat_name
+{% endif %}
\ No newline at end of file
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/template_error.html b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/template_error.html
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/template_error.html
@@ -0,0 +1,13 @@
+{% if entries %}
+    <ul>
+    {% for entry in entries %}
+        <li>
+            {{ entry.key }} 
+            : 
+            {{ entry.invalid_attribute }}
+        </li>
+    {% endfor %}
+    </ul>
+{% else %}
+    <p>No entries are available.</p>
+{% endif %}
\ No newline at end of file
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/template_error2.html b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/template_error2.html
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/templates/my_app/template_error2.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+    <body>
+        {% doesnotexist %}
+    </body>
+</html>
\ No newline at end of file
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/urls.py b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/urls.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/urls.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/urls.py
@@ -5,4 +5,8 @@
 urlpatterns = [
     url(r'^$', views.index, name='index'),
     url(r'^name$', views.get_name, name='name'),
-]
\ No newline at end of file
+    url(r'^template_error$', views.template_error, name='template_error'),
+    url(r'^template_error2$', views.template_error2, name='template_error2'),
+    url(r'^inherits$', views.inherits, name='inherits'),
+    url(r'^no_var_error$', views.no_var_error, name='no_var_error'),
+]
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/views.py b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/views.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/views.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_17/my_app/views.py
@@ -5,6 +5,7 @@
 import sys
 from .forms import NameForm
 
+
 class Entry(object):
 
     def __init__(self, key, val):
@@ -17,6 +18,7 @@ def __unicode__(self):
     def __str__(self):
         return u'%s:%s' % (self.key, self.val)
 
+
 def index(request):
     context = {
         'entries': [Entry('v1', 'v1'), Entry('v2', 'v2')]
@@ -24,6 +26,7 @@ def index(request):
     ret = render(request, 'my_app/index.html', context)
     return ret
 
+
 def get_name(request):
     # if this is a POST request we need to process the form data
     if request.method == 'POST':
@@ -38,6 +41,33 @@ def get_name(request):
 
     # if a GET (or any other method) we'll create a blank form
     else:
-        form = NameForm(data= {'your_name': 'unknown name'})
+        form = NameForm(data={'your_name': 'unknown name'})
+
+    return render(request, 'my_app/name.html', {'form': form})
+
 
-    return render(request, 'my_app/name.html', {'form': form})
\ No newline at end of file
+def inherits(request):
+    context = {}
+    ret = render(request, 'my_app/inherits.html', context)
+    return ret
+
+
+def template_error(request):
+    context = {
+        'entries': [Entry('v1', 'v1'), Entry('v2', 'v2')]
+    }
+
+    ret = render(request, 'my_app/template_error.html', context)
+    return ret
+
+
+def template_error2(request):
+    context = {}
+    ret = render(request, 'my_app/template_error2.html', context)
+    return ret
+
+
+def no_var_error(request):
+    context = {}
+    ret = render(request, 'my_app/no_var_error.html', context)
+    return ret
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/inherited.html b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/inherited.html
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/inherited.html
@@ -0,0 +1,5 @@
+{% if chat_mode %}
+    "chat_mode=True"
+{% else %}
+    "chat_mode=False"
+{% endif %}
\ No newline at end of file
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/inherits.html b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/inherits.html
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/inherits.html
@@ -0,0 +1,2 @@
+{% include 'my_app/inherited.html' with chat_mode=True %}
+{% include 'my_app/inherited.html' with chat_mode=False %}
\ No newline at end of file
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/no_var_error.html b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/no_var_error.html
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/no_var_error.html
@@ -0,0 +1,5 @@
+{% if pat.name %}
+    pat.name={{ pat.name }}
+{% else %}
+    no_pat_name
+{% endif %}
\ No newline at end of file
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/urls.py b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/urls.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/urls.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/urls.py
@@ -7,4 +7,6 @@
     url(r'^name$', views.get_name, name='name'),
     url(r'^template_error2$', views.template_error2, name='template_error2'),
     url(r'^template_error$', views.template_error, name='template_error'),
+    url(r'^inherits$', views.inherits, name='inherits'),
+    url(r'^no_var_error$', views.no_var_error, name='no_var_error'),
 ]
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/views.py b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/views.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/views.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/views.py
@@ -66,3 +66,15 @@ def template_error2(request):
     context = {}
     ret = render(request, 'my_app/template_error2.html', context)
     return ret
+
+
+def inherits(request):
+    context = {}
+    ret = render(request, 'my_app/inherits.html', context)
+    return ret
+
+
+def no_var_error(request):
+    context = {}
+    ret = render(request, 'my_app/no_var_error.html', context)
+    return ret
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
@@ -870,15 +870,10 @@ def test_case_flask(case_setup_flask):
         writer.wait_for_vars(['<var name="content" type="str"'])
         writer.write_run_thread(hit.thread_id)
 
-        for _ in xrange(10):
-            if hasattr(t, 'contents'):
-                break
-            time.sleep(.3)
-        else:
-            raise AssertionError('Flask did not return contents properly!')
+        contents = t.wait_for_contents()
 
-        assert '<title>Hello</title>' in t.contents
-        assert 'Flask-Jinja-Test' in t.contents
+        assert '<title>Hello</title>' in contents
+        assert 'Flask-Jinja-Test' in contents
 
         writer.finished_ok = True
 
@@ -919,14 +914,9 @@ def get_environ(writer):
 
         writer.write_run_thread(hit.thread_id)
 
-        for _ in xrange(10):
-            if hasattr(t, 'contents'):
-                break
-            time.sleep(.3)
-        else:
-            raise AssertionError('Django did not return contents properly!')
+        contents = t.wait_for_contents()
 
-        contents = t.contents.replace(' ', '').replace('\r', '').replace('\n', '')
+        contents = contents.replace(' ', '').replace('\r', '').replace('\n', '')
         if contents != '<ul><li>v1:v1</li><li>v2:v2</li></ul>':
             raise AssertionError('%s != <ul><li>v1:v1</li><li>v2:v2</li></ul>' % (contents,))
 
@@ -954,13 +944,48 @@ def test_case_django_b(case_setup_django):
 
 
 @pytest.mark.skipif(not TEST_DJANGO, reason='No django available')
-@pytest.mark.parametrize("jmc", [False, True])
-def test_case_django_no_attribute_exception_breakpoint(case_setup_django, jmc):
-    django_version = [int(x) for x in django.get_version().split('.')][:2]
+def test_case_django_template_inherits_no_exception(case_setup_django):
+    with case_setup_django.test_file(EXPECTED_RETURNCODE='any') as writer:
+
+        # Check that it doesn't have issues with inherits + django exception breakpoints.
+        writer.write_add_exception_breakpoint_django()
+
+        writer.write_make_initial_run()
+
+        t = writer.create_request_thread('my_app/inherits')
+        time.sleep(5)  # Give django some time to get to startup before requesting the page
+        t.start()
+        contents = t.wait_for_contents()
+
+        contents = contents.replace(' ', '').replace('\r', '').replace('\n', '')
+        assert contents == '''"chat_mode=True""chat_mode=False"'''
+
+        writer.finished_ok = True
+
+
+@pytest.mark.skipif(not TEST_DJANGO, reason='No django available')
+def test_case_django_no_var_error(case_setup_django):
+    with case_setup_django.test_file(EXPECTED_RETURNCODE='any') as writer:
+
+        # Check that it doesn't have issues with inherits + django exception breakpoints.
+        writer.write_add_exception_breakpoint_django()
+
+        writer.write_make_initial_run()
 
-    if django_version < [2, 1]:
-        pytest.skip('Template exceptions only supporting Django 2.1 onwards.')
+        t = writer.create_request_thread('my_app/no_var_error')
+        time.sleep(5)  # Give django some time to get to startup before requesting the page
+        t.start()
+        contents = t.wait_for_contents()
 
+        contents = contents.replace(' ', '').replace('\r', '').replace('\n', '')
+        assert contents == '''no_pat_name'''
+
+        writer.finished_ok = True
+
+
+@pytest.mark.skipif(not TEST_DJANGO, reason='No django available')
+@pytest.mark.parametrize("jmc", [False, True])
+def test_case_django_no_attribute_exception_breakpoint(case_setup_django, jmc):
     kwargs = {}
     if jmc:
 
@@ -993,11 +1018,6 @@ def get_environ(writer):
 
 @pytest.mark.skipif(not TEST_DJANGO, reason='No django available')
 def test_case_django_no_attribute_exception_breakpoint_and_regular_exceptions(case_setup_django):
-    django_version = [int(x) for x in django.get_version().split('.')][:2]
-
-    if django_version < [2, 1]:
-        pytest.skip('Template exceptions only supporting Django 2.1 onwards.')
-
     with case_setup_django.test_file(EXPECTED_RETURNCODE='any') as writer:
         writer.write_add_exception_breakpoint_django()
 
@@ -1026,11 +1046,6 @@ def test_case_django_no_attribute_exception_breakpoint_and_regular_exceptions(ca
 @pytest.mark.skipif(not TEST_DJANGO, reason='No django available')
 @pytest.mark.parametrize("jmc", [False, True])
 def test_case_django_invalid_template_exception_breakpoint(case_setup_django, jmc):
-    django_version = [int(x) for x in django.get_version().split('.')][:2]
-
-    if django_version < [2, 1]:
-        pytest.skip('Template exceptions only supporting Django 2.1 onwards.')
-
     kwargs = {}
     if jmc: