(core) Raise syntax errors that Python can format nicely to show the location

Summary: Update _create_syntax_error_code to raise an error with similar arguments to the real arguments it already has, with our modifications.

Test Plan: Updated python unit tests

Reviewers: jarek, dsagal

Reviewed By: dsagal

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D3040
This commit is contained in:
Alex Hall 2021-09-24 15:06:39 +02:00
parent fb583f303a
commit 52fd28815e
6 changed files with 64 additions and 16 deletions

View File

@ -97,14 +97,16 @@
.error_msg { .error_msg {
color: black; color: black;
cursor: default; cursor: pointer;
margin: 4px; padding: 4px;
} }
.error_details { .error_details {
padding: 2px 2px 2px 2px; padding: 2px 2px 2px 2px;
background-color: #F8ECEA; background-color: #F8ECEA;
margin: 0 0 -2px 0; margin: 0 0 -2px 0;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
} }
.error_box { .error_box {

View File

@ -118,10 +118,11 @@ def _create_syntax_error_code(builder, input_text, err):
output_offset = output_ln.line_to_offset(err.lineno, err.offset - 1 if err.offset else 0) output_offset = output_ln.line_to_offset(err.lineno, err.offset - 1 if err.offset else 0)
input_offset = builder.map_back_offset(output_offset) input_offset = builder.map_back_offset(output_offset)
line, col = input_ln.offset_to_line(input_offset) line, col = input_ln.offset_to_line(input_offset)
message = '%s on line %d col %d' % (err.args[0], line, col + 1) message = err.args[0]
return "%s\nraise %s(%r)" % ( input_text_line = input_text.splitlines()[line - 1]
return "%s\nraise %s(%r, ('usercode', %r, %r, %r))" % (
textbuilder.line_start_re.sub('# ', input_text.rstrip()), textbuilder.line_start_re.sub('# ', input_text.rstrip()),
type(err).__name__, message) type(err).__name__, message, line, col + 1, input_text_line)
#---------------------------------------------------------------------- #----------------------------------------------------------------------

View File

@ -4,6 +4,7 @@ import unittest
import codebuilder import codebuilder
import six import six
unicode_prefix = 'u' if six.PY2 else ''
def make_body(formula, default=None): def make_body(formula, default=None):
return codebuilder.make_formula_body(formula, default).get_text() return codebuilder.make_formula_body(formula, default).get_text()
@ -72,14 +73,20 @@ class TestCodeBuilder(unittest.TestCase):
# Test that we produce valid code when "$foo" occurs in invalid places. # Test that we produce valid code when "$foo" occurs in invalid places.
self.assertEqual(make_body('foo($bar=1)'), self.assertEqual(make_body('foo($bar=1)'),
"# foo($bar=1)\nraise SyntaxError('invalid syntax on line 1 col 5')") "# foo($bar=1)\n"
"raise SyntaxError('invalid syntax', ('usercode', 1, 5, %s'foo($bar=1)'))"
% unicode_prefix)
self.assertEqual(make_body('def $bar(): pass'), self.assertEqual(make_body('def $bar(): pass'),
"# def $bar(): pass\nraise SyntaxError('invalid syntax on line 1 col 5')") "# def $bar(): pass\n"
"raise SyntaxError('invalid syntax', ('usercode', 1, 5, %s'def $bar(): pass'))"
% unicode_prefix)
# If $ is a syntax error, we don't want to turn it into a different syntax error. # If $ is a syntax error, we don't want to turn it into a different syntax error.
self.assertEqual(make_body('$foo + ("$%.2f" $ ($17.5))'), self.assertEqual(make_body('$foo + ("$%.2f" $ ($17.5))'),
'# $foo + ("$%.2f" $ ($17.5))\n' '# $foo + ("$%.2f" $ ($17.5))\n'
"raise SyntaxError('invalid syntax on line 1 col 17')") "raise SyntaxError('invalid syntax', "
"('usercode', 1, 17, {}'$foo + (\"$%.2f\" $ ($17.5))'))"
.format(unicode_prefix))
self.assertEqual(make_body('if $foo:\n' + self.assertEqual(make_body('if $foo:\n' +
' return $foo\n' + ' return $foo\n' +
'else:\n' + 'else:\n' +
@ -88,17 +95,21 @@ class TestCodeBuilder(unittest.TestCase):
'# return $foo\n' + '# return $foo\n' +
'# else:\n' + '# else:\n' +
'# return $ bar\n' + '# return $ bar\n' +
"raise SyntaxError('invalid syntax on line 4 col 10')") "raise SyntaxError('invalid syntax', ('usercode', 4, 10, %s' return $ bar'))"
% unicode_prefix)
# Check for reasonable behaviour with non-empty text and no statements. # Check for reasonable behaviour with non-empty text and no statements.
self.assertEqual(make_body('# comment'), '# comment\npass') self.assertEqual(make_body('# comment'), '# comment\npass')
self.assertEqual(make_body('rec = 1'), "# rec = 1\n" + self.assertEqual(make_body('rec = 1'), "# rec = 1\n" +
"raise SyntaxError('Grist disallows assignment " + "raise SyntaxError('Grist disallows assignment " +
"to the special variable \"rec\" on line 1 col 1')") "to the special variable \"rec\"', ('usercode', 1, 1, %s'rec = 1'))"
% unicode_prefix)
self.assertEqual(make_body('for rec in []: pass'), "# for rec in []: pass\n" + self.assertEqual(make_body('for rec in []: pass'), "# for rec in []: pass\n" +
"raise SyntaxError('Grist disallows assignment " + "raise SyntaxError('Grist disallows assignment " +
"to the special variable \"rec\" on line 1 col 4')") "to the special variable \"rec\"', "
"('usercode', 1, 4, %s'for rec in []: pass'))"
% unicode_prefix)
# some legitimates use of rec # some legitimates use of rec
body = (""" body = ("""
@ -125,7 +136,9 @@ return rec
self.assertRegex(make_body(body), self.assertRegex(make_body(body),
r"raise SyntaxError\('Grist disallows assignment" + r"raise SyntaxError\('Grist disallows assignment" +
r" to the special variable \"rec\" on line 4 col 7'\)") r" to the special variable \"rec\"', "
r"\('usercode', 4, 7, %s'\[1 for rec in \[\]\]'\)\)"
% unicode_prefix)
def test_make_formula_body_unicode(self): def test_make_formula_body_unicode(self):

View File

@ -76,10 +76,36 @@ else:
)) ))
self.assertFormulaError(self.engine.get_formula_error('Math', 'syntax_err', 3), self.assertFormulaError(self.engine.get_formula_error('Math', 'syntax_err', 3),
SyntaxError, "invalid syntax on line 5 col 9") SyntaxError, "invalid syntax (usercode, line 5)",
textwrap.dedent(
r"""
File "usercode", line 5
return: 0
\^
SyntaxError: invalid syntax
"""
))
if six.PY2:
traceback_regex = textwrap.dedent(
r"""
File "usercode", line 2
if sum\(3, 5\) > 6:
\^
IndentationError: unexpected indent
"""
)
else:
traceback_regex = textwrap.dedent(
r"""
File "usercode", line 2
if sum\(3, 5\) > 6:
IndentationError: unexpected indent
"""
)
self.assertFormulaError(self.engine.get_formula_error('Math', 'indent_err', 3), self.assertFormulaError(self.engine.get_formula_error('Math', 'indent_err', 3),
IndentationError, "unexpected indent on line 2 col 2") IndentationError, 'unexpected indent (usercode, line 2)',
traceback_regex)
self.assertFormulaError(self.engine.get_formula_error('Math', 'other_err', 3), self.assertFormulaError(self.engine.get_formula_error('Math', 'other_err', 3),
TypeError, "'int' object is not iterable", TypeError, "'int' object is not iterable",

View File

@ -4,6 +4,7 @@ import unittest
import difflib import difflib
import re import re
import six
from six.moves import xrange from six.moves import xrange
import gencode import gencode
@ -69,6 +70,11 @@ class TestGenCode(unittest.TestCase):
gcode = gencode.GenCode() gcode = gencode.GenCode()
gcode.make_module(self.schema) gcode.make_module(self.schema)
generated = gcode.get_user_text() generated = gcode.get_user_text()
if six.PY3:
generated = generated.replace(
", 'for a in b'))",
", u'for a in b'))",
)
self.assertEqual(generated, saved_sample, "Generated code doesn't match sample:\n" + self.assertEqual(generated, saved_sample, "Generated code doesn't match sample:\n" +
"".join(difflib.unified_diff(generated.splitlines(True), "".join(difflib.unified_diff(generated.splitlines(True),
saved_sample.splitlines(True), saved_sample.splitlines(True),

View File

@ -63,6 +63,6 @@ class Address:
def badSyntax(rec, table): def badSyntax(rec, table):
# for a in b # for a in b
# 10 # 10
raise SyntaxError('invalid syntax on line 1 col 11') raise SyntaxError('invalid syntax', ('usercode', 1, 11, u'for a in b'))
====================================================================== ======================================================================
""" """