bpo-43914: Highlight invalid ranges in SyntaxErrors (#25525)

To improve the user experience understanding what part of the error messages associated with SyntaxErrors is wrong, we can highlight the whole error range and not only place the caret at the first character. In this way:

>>> foo(x, z for z in range(10), t, w)
  File "<stdin>", line 1
    foo(x, z for z in range(10), t, w)
           ^
SyntaxError: Generator expression must be parenthesized

becomes

>>> foo(x, z for z in range(10), t, w)
  File "<stdin>", line 1
    foo(x, z for z in range(10), t, w)
           ^^^^^^^^^^^^^^^^^^^^
SyntaxError: Generator expression must be parenthesized
This commit is contained in:
Pablo Galindo 2021-04-23 14:27:05 +01:00 committed by GitHub
parent 91b69b77cf
commit a77aac4fca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1687 additions and 1219 deletions

View file

@ -8,6 +8,7 @@ import unittest
import pickle
import weakref
import errno
from textwrap import dedent
from test.support import (captured_stderr, check_impl_detail,
cpython_only, gc_collect,
@ -255,13 +256,13 @@ class ExceptionTests(unittest.TestCase):
check('from __future__ import doesnt_exist', 1, 1)
check('from __future__ import braces', 1, 1)
check('x=1\nfrom __future__ import division', 2, 1)
check('foo(1=2)', 1, 6)
check('foo(1=2)', 1, 5)
check('def f():\n x, y: int', 2, 3)
check('[*x for x in xs]', 1, 2)
check('foo(x for x in range(10), 100)', 1, 5)
check('for 1 in []: pass', 1, 5)
check('(yield i) = 2', 1, 11)
check('def f(*):\n pass', 1, 8)
check('(yield i) = 2', 1, 2)
check('def f(*):\n pass', 1, 7)
@cpython_only
def testSettingException(self):
@ -395,25 +396,31 @@ class ExceptionTests(unittest.TestCase):
'filename' : 'filenameStr', 'filename2' : None}),
(SyntaxError, (), {'msg' : None, 'text' : None,
'filename' : None, 'lineno' : None, 'offset' : None,
'print_file_and_line' : None}),
'end_offset': None, 'print_file_and_line' : None}),
(SyntaxError, ('msgStr',),
{'args' : ('msgStr',), 'text' : None,
'print_file_and_line' : None, 'msg' : 'msgStr',
'filename' : None, 'lineno' : None, 'offset' : None}),
'filename' : None, 'lineno' : None, 'offset' : None,
'end_offset': None}),
(SyntaxError, ('msgStr', ('filenameStr', 'linenoStr', 'offsetStr',
'textStr')),
'textStr', 'endLinenoStr', 'endOffsetStr')),
{'offset' : 'offsetStr', 'text' : 'textStr',
'args' : ('msgStr', ('filenameStr', 'linenoStr',
'offsetStr', 'textStr')),
'offsetStr', 'textStr',
'endLinenoStr', 'endOffsetStr')),
'print_file_and_line' : None, 'msg' : 'msgStr',
'filename' : 'filenameStr', 'lineno' : 'linenoStr'}),
'filename' : 'filenameStr', 'lineno' : 'linenoStr',
'end_lineno': 'endLinenoStr', 'end_offset': 'endOffsetStr'}),
(SyntaxError, ('msgStr', 'filenameStr', 'linenoStr', 'offsetStr',
'textStr', 'print_file_and_lineStr'),
'textStr', 'endLinenoStr', 'endOffsetStr',
'print_file_and_lineStr'),
{'text' : None,
'args' : ('msgStr', 'filenameStr', 'linenoStr', 'offsetStr',
'textStr', 'print_file_and_lineStr'),
'textStr', 'endLinenoStr', 'endOffsetStr',
'print_file_and_lineStr'),
'print_file_and_line' : None, 'msg' : 'msgStr',
'filename' : None, 'lineno' : None, 'offset' : None}),
'filename' : None, 'lineno' : None, 'offset' : None,
'end_lineno': None, 'end_offset': None}),
(UnicodeError, (), {'args' : (),}),
(UnicodeEncodeError, ('ascii', 'a', 0, 1,
'ordinal not in range'),
@ -459,7 +466,7 @@ class ExceptionTests(unittest.TestCase):
e = exc(*args)
except:
print("\nexc=%r, args=%r" % (exc, args), file=sys.stderr)
raise
# raise
else:
# Verify module name
if not type(e).__name__.endswith('NaiveException'):
@ -1827,6 +1834,130 @@ class ImportErrorTests(unittest.TestCase):
self.assertEqual(exc.name, orig.name)
self.assertEqual(exc.path, orig.path)
class SyntaxErrorTests(unittest.TestCase):
def test_range_of_offsets(self):
cases = [
# Basic range from 2->7
(("bad.py", 1, 2, "abcdefg", 1, 7),
dedent(
"""
File "bad.py", line 1
abcdefg
^^^^^
SyntaxError: bad bad
""")),
# end_offset = start_offset + 1
(("bad.py", 1, 2, "abcdefg", 1, 3),
dedent(
"""
File "bad.py", line 1
abcdefg
^
SyntaxError: bad bad
""")),
# Negative end offset
(("bad.py", 1, 2, "abcdefg", 1, -2),
dedent(
"""
File "bad.py", line 1
abcdefg
^
SyntaxError: bad bad
""")),
# end offset before starting offset
(("bad.py", 1, 4, "abcdefg", 1, 2),
dedent(
"""
File "bad.py", line 1
abcdefg
^
SyntaxError: bad bad
""")),
# Both offsets negative
(("bad.py", 1, -4, "abcdefg", 1, -2),
dedent(
"""
File "bad.py", line 1
abcdefg
SyntaxError: bad bad
""")),
# Both offsets negative and the end more negative
(("bad.py", 1, -4, "abcdefg", 1, -5),
dedent(
"""
File "bad.py", line 1
abcdefg
SyntaxError: bad bad
""")),
# Both offsets 0
(("bad.py", 1, 0, "abcdefg", 1, 0),
dedent(
"""
File "bad.py", line 1
abcdefg
SyntaxError: bad bad
""")),
# Start offset 0 and end offset not 0
(("bad.py", 1, 0, "abcdefg", 1, 5),
dedent(
"""
File "bad.py", line 1
abcdefg
SyntaxError: bad bad
""")),
# End offset pass the source lenght
(("bad.py", 1, 2, "abcdefg", 1, 100),
dedent(
"""
File "bad.py", line 1
abcdefg
^^^^^^
SyntaxError: bad bad
""")),
]
for args, expected in cases:
with self.subTest(args=args):
try:
raise SyntaxError("bad bad", args)
except SyntaxError as exc:
with support.captured_stderr() as err:
sys.__excepthook__(*sys.exc_info())
the_exception = exc
def test_attributes_new_constructor(self):
args = ("bad.py", 1, 2, "abcdefg", 1, 100)
the_exception = SyntaxError("bad bad", args)
filename, lineno, offset, error, end_lineno, end_offset = args
self.assertEqual(filename, the_exception.filename)
self.assertEqual(lineno, the_exception.lineno)
self.assertEqual(end_lineno, the_exception.end_lineno)
self.assertEqual(offset, the_exception.offset)
self.assertEqual(end_offset, the_exception.end_offset)
self.assertEqual(error, the_exception.text)
self.assertEqual("bad bad", the_exception.msg)
def test_attributes_old_constructor(self):
args = ("bad.py", 1, 2, "abcdefg")
the_exception = SyntaxError("bad bad", args)
filename, lineno, offset, error = args
self.assertEqual(filename, the_exception.filename)
self.assertEqual(lineno, the_exception.lineno)
self.assertEqual(None, the_exception.end_lineno)
self.assertEqual(offset, the_exception.offset)
self.assertEqual(None, the_exception.end_offset)
self.assertEqual(error, the_exception.text)
self.assertEqual("bad bad", the_exception.msg)
def test_incorrect_constructor(self):
args = ("bad.py", 1, 2)
self.assertRaises(TypeError, SyntaxError, "bad bad", args)
args = ("bad.py", 1, 2, 4, 5, 6, 7)
self.assertRaises(TypeError, SyntaxError, "bad bad", args)
args = ("bad.py", 1, 2, "abcdefg", 1)
self.assertRaises(TypeError, SyntaxError, "bad bad", args)
class PEP626Tests(unittest.TestCase):