(core) Error explanations from friendly-traceback

Summary: Extend formula error messages with explanations from https://github.com/friendly-traceback/friendly-traceback. Only for Python 3.

Test Plan: Updated several Python tests. In general, these require separate branches for Python 2 and 3.

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3542
This commit is contained in:
Alex Hall
2022-08-11 13:03:45 +02:00
parent 31f54065f5
commit 49cb51bac5
11 changed files with 239 additions and 64 deletions

View File

@@ -1,11 +1,14 @@
import ast
import contextlib
import itertools
import linecache
import re
import six
import astroid
import asttokens
import friendly_errors
import textbuilder
import logger
log = logger.Logger(__name__, logger.INFO)
@@ -22,6 +25,13 @@ LAZY_ARG_FUNCTIONS = {
'PEEK': slice(0, 1),
}
class GristSyntaxError(SyntaxError):
"""
Indicates a formula is invalid in a Grist-specific way.
"""
def make_formula_body(formula, default_value, assoc_value=None):
"""
Given a formula, returns a textbuilder.Builder object suitable to be the body of a function,
@@ -46,7 +56,7 @@ def make_formula_body(formula, default_value, assoc_value=None):
# Parse the formula into an abstract syntax tree (AST), catching syntax errors.
try:
atok = asttokens.ASTTokens(tmp_formula.get_text(), parse=True)
atok = asttokens.ASTTokens(tmp_formula.get_text(), parse=True, filename=code_filename)
except SyntaxError as e:
return textbuilder.Text(_create_syntax_error_code(tmp_formula, formula, e))
@@ -91,8 +101,7 @@ def make_formula_body(formula, default_value, assoc_value=None):
message = "No `return` statement, and the last line isn't an expression."
if isinstance(last_statement, ast.Assign):
message += " If you want to check for equality, use `==` instead of `=`."
error = SyntaxError(message,
('<string>', 1, 1, ""))
error = GristSyntaxError(message, ('<string>', 1, 1, ""))
return textbuilder.Text(_create_syntax_error_code(tmp_formula, formula, error))
# Apply the new set of patches to the original formula to get the real output.
@@ -129,11 +138,24 @@ 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)
input_offset = builder.map_back_offset(output_offset)
line, col = input_ln.offset_to_line(input_offset)
message = err.args[0]
input_text_line = input_text.splitlines()[line - 1]
message = err.args[0]
err_type = type(err)
if isinstance(err, GristSyntaxError):
# Just use SyntaxError in the final code
err_type = SyntaxError
elif six.PY3:
# Add explanation from friendly-traceback.
# Only supported in Python 3.
# Not helpful for Grist-specific errors.
# Needs to use the source code, so save it to its source cache.
save_to_linecache(builder.get_text())
message += friendly_errors.friendly_message(err)
return "%s\nraise %s(%r, ('usercode', %r, %r, %r))" % (
textbuilder.line_start_re.sub('# ', input_text.rstrip()),
type(err).__name__, message, line, col + 1, input_text_line)
err_type.__name__, message, line, col + 1, input_text_line)
#----------------------------------------------------------------------
@@ -299,7 +321,7 @@ class InferRecAssignment(InferenceTip):
@classmethod
def filter(cls, node):
if node.name == 'rec':
raise SyntaxError('Grist disallows assignment to the special variable "rec"',
raise GristSyntaxError('Grist disallows assignment to the special variable "rec"',
('<string>', node.lineno, node.col_offset, ""))
@classmethod
@@ -315,8 +337,8 @@ class InferRecAttrAssignment(InferenceTip):
@classmethod
def filter(cls, node):
if isinstance(node.expr, astroid.nodes.Name) and node.expr.name == 'rec':
raise SyntaxError("You can't assign a value to a column with `=`. "
"If you mean to check for equality, use `==` instead.",
raise GristSyntaxError("You can't assign a value to a column with `=`. "
"If you mean to check for equality, use `==` instead.",
('<string>', node.lineno, node.col_offset, ""))
@classmethod
@@ -379,3 +401,23 @@ def parse_grist_names(builder):
parsed_names.append(make_tuple(start, end, obj.name, node.arg))
return [name for name in parsed_names if name]
code_filename = "usercode"
def save_to_linecache(source_code):
"""
Makes source code available to friendly-traceback and traceback formatting in general.
"""
if six.PY3:
import friendly_traceback.source_cache
friendly_traceback.source_cache.cache.add(code_filename, source_code)
else:
linecache.cache[code_filename] = (
len(source_code),
None,
[line + '\n' for line in source_code.splitlines()],
code_filename,
)