Jelajahi Sumber

Fixed #33701 -- Added fine-grained error locations to the technical 500 debug page.

Giebisch 2 tahun lalu
induk
melakukan
85b52d22fd

+ 1 - 0
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>

+ 22 - 1
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
 

+ 2 - 2
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 %}

+ 1 - 1
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 %}

+ 2 - 1
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
 ~~~~~~~~~~~~

+ 73 - 0
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()"