From 48856ead6ae023b2819ee63cb6ff97a0976a2cc3 Mon Sep 17 00:00:00 2001
From: Pablo Galindo Salgado <Pablogsal@gmail.com>
Date: Mon, 19 Aug 2024 17:09:03 +0200
Subject: gh-123123: Fix display of syntax errors covering multiple lines
 (#123131)

Signed-off-by: Pablo Galindo <pablogsal@gmail.com>
---
 Lib/test/test_traceback.py                         | 40 +++++++++++++++++++---
 Lib/traceback.py                                   | 10 ++++--
 .../2024-08-18-18-25-54.gh-issue-123123.0ZcaEB.rst |  2 ++
 3 files changed, 45 insertions(+), 7 deletions(-)
 create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-08-18-18-25-54.gh-issue-123123.0ZcaEB.rst

diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 1895c88..b568221 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -699,6 +699,35 @@ class TracebackErrorLocationCaretTestBase:
         result_lines = self.get_exception(f_with_multiline)
         self.assertEqual(result_lines, expected_f.splitlines())
 
+        # Check custom error messages covering multiple lines
+        code = textwrap.dedent("""
+        dummy_call(
+            "dummy value"
+            foo="bar",
+        )
+        """)
+
+        def f_with_multiline():
+            # Need to defer the compilation until in self.get_exception(..)
+            return compile(code, "?", "exec")
+
+        lineno_f = f_with_multiline.__code__.co_firstlineno
+
+        expected_f = (
+            'Traceback (most recent call last):\n'
+            f'  File "{__file__}", line {self.callable_line}, in get_exception\n'
+            '    callable()\n'
+            '    ~~~~~~~~^^\n'
+            f'  File "{__file__}", line {lineno_f+2}, in f_with_multiline\n'
+            '    return compile(code, "?", "exec")\n'
+            '  File "?", line 3\n'
+            '    "dummy value"\n'
+            '    ^^^^^^^^^^^^^'
+            )
+
+        result_lines = self.get_exception(f_with_multiline)
+        self.assertEqual(result_lines, expected_f.splitlines())
+
     def test_caret_multiline_expression_bin_op(self):
         # Make sure no carets are printed for expressions spanning multiple
         # lines.
@@ -2312,19 +2341,22 @@ class BaseExceptionReportingTests:
     def test_syntax_error_various_offsets(self):
         for offset in range(-5, 10):
             for add in [0, 2]:
-                text = " "*add + "text%d" % offset
+                text = " " * add + "text%d" % offset
                 expected = ['  File "file.py", line 1']
                 if offset < 1:
                     expected.append("    %s" % text.lstrip())
                 elif offset <= 6:
                     expected.append("    %s" % text.lstrip())
-                    expected.append("    %s^" % (" "*(offset-1)))
+                    # Set the caret length to match the length of the text minus the offset.
+                    caret_length = max(1, len(text.lstrip()) - offset + 1)
+                    expected.append("    %s%s" % (" " * (offset - 1), "^" * caret_length))
                 else:
+                    caret_length = max(1, len(text.lstrip()) - 4)
                     expected.append("    %s" % text.lstrip())
-                    expected.append("    %s^" % (" "*5))
+                    expected.append("    %s%s" % (" " * 5, "^" * caret_length))
                 expected.append("SyntaxError: msg")
                 expected.append("")
-                err = self.get_report(SyntaxError("msg", ("file.py", 1, offset+add, text)))
+                err = self.get_report(SyntaxError("msg", ("file.py", 1, offset + add, text)))
                 exp = "\n".join(expected)
                 self.assertEqual(exp, err)
 
diff --git a/Lib/traceback.py b/Lib/traceback.py
index 6ee1a50..3e708c6 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -1292,11 +1292,15 @@ class TracebackException:
                 yield '    {}\n'.format(ltext)
             else:
                 offset = self.offset
-                end_offset = self.end_offset if self.end_offset not in {None, 0} else offset
+                if self.lineno == self.end_lineno:
+                    end_offset = self.end_offset if self.end_offset not in {None, 0} else offset
+                else:
+                    end_offset = len(rtext) + 1
+
                 if self.text and offset > len(self.text):
-                    offset = len(self.text) + 1
+                    offset = len(rtext) + 1
                 if self.text and end_offset > len(self.text):
-                    end_offset = len(self.text) + 1
+                    end_offset = len(rtext) + 1
                 if offset >= end_offset or end_offset < 0:
                     end_offset = offset + 1
 
diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-08-18-18-25-54.gh-issue-123123.0ZcaEB.rst b/Misc/NEWS.d/next/Core and Builtins/2024-08-18-18-25-54.gh-issue-123123.0ZcaEB.rst
new file mode 100644
index 0000000..824d307
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2024-08-18-18-25-54.gh-issue-123123.0ZcaEB.rst	
@@ -0,0 +1,2 @@
+Fix displaying :exc:`SyntaxError` exceptions covering multiple lines. Patch
+by Pablo Galindo
-- 
cgit v0.12