diff options
| author | Giebisch <rafael@giebisch-mail.de> | 2022-11-03 11:49:10 +0100 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2022-11-29 08:40:11 +0100 |
| commit | 85b52d22fd2841c34e95b3a80d6f2b668ce2f160 (patch) | |
| tree | 957e84409a0ca2135e79ae202005732dfbf3fdd8 | |
| parent | 9d726c7902979d4ad53945ed8f1037266a88010d (diff) | |
Fixed #33701 -- Added fine-grained error locations to the technical 500 debug page.
| -rw-r--r-- | AUTHORS | 1 | ||||
| -rw-r--r-- | django/views/debug.py | 23 | ||||
| -rw-r--r-- | django/views/templates/technical_500.html | 4 | ||||
| -rw-r--r-- | django/views/templates/technical_500.txt | 2 | ||||
| -rw-r--r-- | docs/releases/4.2.txt | 3 | ||||
| -rw-r--r-- | tests/view_tests/tests/test_debug.py | 73 |
6 files changed, 101 insertions, 5 deletions
@@ -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("Top level")\n" + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</pre>", + html, + ) + self.assertIn( + "<pre> raise ValueError(mark_safe(" + ""<p>2nd exception</p>")) from explicit\n" + " " + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</pre>", + html, + ) + self.assertIn( + "<pre> raise IndexError("Final exception")\n" + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</pre>", + html, + ) + # Pastebin. + self.assertIn( + " raise AttributeError("Top level")\n" + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + html, + ) + self.assertIn( + " raise ValueError(mark_safe(" + ""<p>2nd exception</p>")) from explicit\n" + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + html, + ) + self.assertIn( + " raise IndexError("Final exception")\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()" |
