summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGiebisch <rafael@giebisch-mail.de>2022-11-03 11:49:10 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-11-29 08:40:11 +0100
commit85b52d22fd2841c34e95b3a80d6f2b668ce2f160 (patch)
tree957e84409a0ca2135e79ae202005732dfbf3fdd8
parent9d726c7902979d4ad53945ed8f1037266a88010d (diff)
Fixed #33701 -- Added fine-grained error locations to the technical 500 debug page.
-rw-r--r--AUTHORS1
-rw-r--r--django/views/debug.py23
-rw-r--r--django/views/templates/technical_500.html4
-rw-r--r--django/views/templates/technical_500.txt2
-rw-r--r--docs/releases/4.2.txt3
-rw-r--r--tests/view_tests/tests/test_debug.py73
6 files changed, 101 insertions, 5 deletions
diff --git a/AUTHORS b/AUTHORS
index 5b9d8621d9..cccdafa4b9 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -802,6 +802,7 @@ answer newbie questions, and generally made Django that much better:
Rachel Tobin <rmtobin@me.com>
Rachel Willmer <http://www.willmer.com/kb/>
Radek Švarz <https://www.svarz.cz/translate/>
+ Rafael Giebisch <rafael@giebisch-mail.de>
Raffaele Salmaso <raffaele@salmaso.org>
Rajesh Dhawan <rajesh.dhawan@gmail.com>
Ramez Ashraf <ramezashraf@gmail.com>
diff --git a/django/views/debug.py b/django/views/debug.py
index b93afa5737..53b4125716 100644
--- a/django/views/debug.py
+++ b/django/views/debug.py
@@ -1,4 +1,5 @@
import functools
+import itertools
import re
import sys
import types
@@ -15,7 +16,7 @@ from django.utils.datastructures import MultiValueDict
from django.utils.encoding import force_str
from django.utils.module_loading import import_string
from django.utils.regex_helper import _lazy_re_compile
-from django.utils.version import get_docs_version
+from django.utils.version import PY311, get_docs_version
# Minimal Django templates engine to render the error templates
# regardless of the project's TEMPLATES setting. Templates are
@@ -546,6 +547,24 @@ class ExceptionReporter:
pre_context = []
context_line = "<source code not available>"
post_context = []
+
+ colno = tb_area_colno = ""
+ if PY311:
+ _, _, start_column, end_column = next(
+ itertools.islice(
+ tb.tb_frame.f_code.co_positions(), tb.tb_lasti // 2, None
+ )
+ )
+ if start_column and end_column:
+ underline = "^" * (end_column - start_column)
+ spaces = " " * (start_column + len(str(lineno + 1)) + 2)
+ colno = f"\n{spaces}{underline}"
+ tb_area_spaces = " " * (
+ 4
+ + start_column
+ - (len(context_line) - len(context_line.lstrip()))
+ )
+ tb_area_colno = f"\n{tb_area_spaces}{underline}"
yield {
"exc_cause": exc_cause,
"exc_cause_explicit": exc_cause_explicit,
@@ -562,6 +581,8 @@ class ExceptionReporter:
"context_line": context_line,
"post_context": post_context,
"pre_context_lineno": pre_context_lineno + 1,
+ "colno": colno,
+ "tb_area_colno": tb_area_colno,
}
tb = tb.tb_next
diff --git a/django/views/templates/technical_500.html b/django/views/templates/technical_500.html
index ae0411729a..a5c187147b 100644
--- a/django/views/templates/technical_500.html
+++ b/django/views/templates/technical_500.html
@@ -242,7 +242,7 @@
</ol>
{% endif %}
<ol start="{{ frame.lineno }}" class="context-line">
- <li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre>{{ frame.context_line }}</pre>{% if not is_email %} <span>…</span>{% endif %}</li>
+ <li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre>{{ frame.context_line }}{{ frame.colno }}</pre>{% if not is_email %} <span>…</span>{% endif %}</li>
</ol>
{% if frame.post_context and not is_email %}
<ol start='{{ frame.lineno|add:"1" }}' class="post-context" id="post{{ frame.id }}">
@@ -327,7 +327,7 @@ The above exception ({{ frame.exc_cause|force_escape }}) was the direct cause of
{% else %}
During handling of the above exception ({{ frame.exc_cause|force_escape }}), another exception occurred:
{% endif %}{% endif %}{% endifchanged %} {% if frame.tb %}File "{{ frame.filename }}"{% if frame.context_line %}, line {{ frame.lineno }}{% endif %}, in {{ frame.function }}
-{% if frame.context_line %} {% spaceless %}{{ frame.context_line }}{% endspaceless %}{% endif %}{% elif forloop.first %}None{% else %}Traceback: None{% endif %}{% endfor %}
+{% if frame.context_line %} {% spaceless %}{{ frame.context_line }}{% endspaceless %}{{ frame.tb_area_colno }}{% endif %}{% elif forloop.first %}None{% else %}Traceback: None{% endif %}{% endfor %}
Exception Type: {{ exception_type }}{% if request %} at {{ request.path_info }}{% endif %}
Exception Value: {{ exception_value|force_escape }}{% if exception_notes %}{{ exception_notes }}{% endif %}
diff --git a/django/views/templates/technical_500.txt b/django/views/templates/technical_500.txt
index a481c5db0d..5a75324ebc 100644
--- a/django/views/templates/technical_500.txt
+++ b/django/views/templates/technical_500.txt
@@ -31,7 +31,7 @@ Traceback (most recent call last):
{% for frame in frames %}{% ifchanged frame.exc_cause %}{% if frame.exc_cause %}
{% if frame.exc_cause_explicit %}The above exception ({{ frame.exc_cause }}) was the direct cause of the following exception:{% else %}During handling of the above exception ({{ frame.exc_cause }}), another exception occurred:{% endif %}
{% endif %}{% endifchanged %} {% if frame.tb %}File "{{ frame.filename }}"{% if frame.context_line %}, line {{ frame.lineno }}{% endif %}, in {{ frame.function }}
-{% if frame.context_line %} {% spaceless %}{{ frame.context_line }}{% endspaceless %}{% endif %}{% elif forloop.first %}None{% else %}Traceback: None{% endif %}
+{% if frame.context_line %} {% spaceless %}{{ frame.context_line }}{% endspaceless %}{{ frame.tb_area_colno }}{% endif %}{% elif forloop.first %}None{% else %}Traceback: None{% endif %}
{% endfor %}
{% if exception_type %}Exception Type: {{ exception_type }}{% if request %} at {{ request.path_info }}{% endif %}
{% if exception_value %}Exception Value: {{ exception_value }}{% endif %}{% if exception_notes %}{{ exception_notes }}{% endif %}{% endif %}{% endif %}
diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt
index fe74e8485a..8153eefebc 100644
--- a/docs/releases/4.2.txt
+++ b/docs/releases/4.2.txt
@@ -161,7 +161,8 @@ Email
Error Reporting
~~~~~~~~~~~~~~~
-* The debug page now shows :pep:`exception notes <678>` on Python 3.11+.
+* The debug page now shows :pep:`exception notes <678>` and
+ :pep:`fine-grained error locations <657>` on Python 3.11+.
File Storage
~~~~~~~~~~~~
diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py
index a9b7625617..020ac7193e 100644
--- a/tests/view_tests/tests/test_debug.py
+++ b/tests/view_tests/tests/test_debug.py
@@ -766,6 +766,79 @@ class ExceptionReporterTests(SimpleTestCase):
self.assertIn(implicit_exc.format("<p>Second exception</p>"), text)
self.assertEqual(3, text.count("<p>Final exception</p>"))
+ @skipIf(
+ sys._xoptions.get("no_debug_ranges", False)
+ or os.environ.get("PYTHONNODEBUGRANGES", False),
+ "Fine-grained error locations are disabled.",
+ )
+ @skipUnless(PY311, "Fine-grained error locations were added in Python 3.11.")
+ def test_highlight_error_position(self):
+ request = self.rf.get("/test_view/")
+ try:
+ try:
+ raise AttributeError("Top level")
+ except AttributeError as explicit:
+ try:
+ raise ValueError(mark_safe("<p>2nd exception</p>")) from explicit
+ except ValueError:
+ raise IndexError("Final exception")
+ except Exception:
+ exc_type, exc_value, tb = sys.exc_info()
+
+ reporter = ExceptionReporter(request, exc_type, exc_value, tb)
+ html = reporter.get_traceback_html()
+ self.assertIn(
+ "<pre> raise AttributeError(&quot;Top level&quot;)\n"
+ " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</pre>",
+ html,
+ )
+ self.assertIn(
+ "<pre> raise ValueError(mark_safe("
+ "&quot;&lt;p&gt;2nd exception&lt;/p&gt;&quot;)) from explicit\n"
+ " "
+ "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</pre>",
+ html,
+ )
+ self.assertIn(
+ "<pre> raise IndexError(&quot;Final exception&quot;)\n"
+ " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</pre>",
+ html,
+ )
+ # Pastebin.
+ self.assertIn(
+ " raise AttributeError(&quot;Top level&quot;)\n"
+ " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+ html,
+ )
+ self.assertIn(
+ " raise ValueError(mark_safe("
+ "&quot;&lt;p&gt;2nd exception&lt;/p&gt;&quot;)) from explicit\n"
+ " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+ html,
+ )
+ self.assertIn(
+ " raise IndexError(&quot;Final exception&quot;)\n"
+ " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+ html,
+ )
+ # Text traceback.
+ text = reporter.get_traceback_text()
+ self.assertIn(
+ ' raise AttributeError("Top level")\n'
+ " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+ text,
+ )
+ self.assertIn(
+ ' raise ValueError(mark_safe("<p>2nd exception</p>")) from explicit\n'
+ " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+ text,
+ )
+ self.assertIn(
+ ' raise IndexError("Final exception")\n'
+ " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+ text,
+ )
+
def test_reporting_frames_without_source(self):
try:
source = "def funcName():\n raise Error('Whoops')\nfuncName()"