mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(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:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user