mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +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:
parent
31f54065f5
commit
49cb51bac5
@ -5,15 +5,20 @@ import {arrayToString} from 'app/common/arrayToString';
|
||||
import * as marshal from 'app/common/marshal';
|
||||
import {ISandbox, ISandboxCreationOptions, ISandboxCreator} from 'app/server/lib/ISandbox';
|
||||
import log from 'app/server/lib/log';
|
||||
import {DirectProcessControl, ISandboxControl, NoProcessControl, ProcessInfo,
|
||||
SubprocessControl} from 'app/server/lib/SandboxControl';
|
||||
import {
|
||||
DirectProcessControl,
|
||||
ISandboxControl,
|
||||
NoProcessControl,
|
||||
ProcessInfo,
|
||||
SubprocessControl
|
||||
} from 'app/server/lib/SandboxControl';
|
||||
import * as sandboxUtil from 'app/server/lib/sandboxUtil';
|
||||
import * as shutdown from 'app/server/lib/shutdown';
|
||||
import {ChildProcess, spawn} from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import {Stream, Writable} from 'stream';
|
||||
import * as _ from 'lodash';
|
||||
import * as fs from 'fs';
|
||||
import * as which from 'which';
|
||||
|
||||
type SandboxMethod = (...args: any[]) => any;
|
||||
@ -570,15 +575,9 @@ function gvisor(options: ISandboxOptions): SandboxProcess {
|
||||
// Check for local virtual environments created with core's
|
||||
// install:python2 or install:python3 targets. They'll need
|
||||
// some extra sharing to make available in the sandbox.
|
||||
// This appears to currently be incompatible with checkpoints?
|
||||
// Shares and checkpoints interact delicately because the file
|
||||
// handle layout/ordering needs to remain exactly the same.
|
||||
// Fixable no doubt, but for now I just disable this convenience
|
||||
// if checkpoints are in use.
|
||||
const venv = path.join(process.cwd(),
|
||||
pythonVersion === '2' ? 'venv' : 'sandbox_venv3');
|
||||
const useCheckpoint = process.env.GRIST_CHECKPOINT && !paths.importDir;
|
||||
if (fs.existsSync(venv) && !useCheckpoint) {
|
||||
if (fs.existsSync(venv)) {
|
||||
wrapperArgs.addMount(venv);
|
||||
wrapperArgs.push('-s', path.join(venv, 'bin', 'python'));
|
||||
}
|
||||
@ -589,7 +588,7 @@ function gvisor(options: ISandboxOptions): SandboxProcess {
|
||||
// between the checkpoint and how it gets used later).
|
||||
// If a sandbox is being used for import, it will have a special mount we can't
|
||||
// deal with easily right now. Should be possible to do in future if desired.
|
||||
if (options.useGristEntrypoint && pythonVersion === '3' && useCheckpoint) {
|
||||
if (options.useGristEntrypoint && pythonVersion === '3' && process.env.GRIST_CHECKPOINT && !paths.importDir) {
|
||||
if (process.env.GRIST_CHECKPOINT_MAKE) {
|
||||
const child =
|
||||
spawn(command, [...wrapperArgs.get(), '--checkpoint', process.env.GRIST_CHECKPOINT!,
|
||||
|
@ -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,
|
||||
)
|
||||
|
43
sandbox/grist/friendly_errors.py
Normal file
43
sandbox/grist/friendly_errors.py
Normal file
@ -0,0 +1,43 @@
|
||||
def friendly_message(exc):
|
||||
"""
|
||||
Returns a string to append to a standard error message.
|
||||
If possible, the string contains a friendly explanation of the error.
|
||||
Otherwise, the string is empty.
|
||||
"""
|
||||
try:
|
||||
if "has no column" in str(exc):
|
||||
# Avoid the standard AttributeError explanation
|
||||
return ""
|
||||
|
||||
# Imported locally because it's Python 3 only
|
||||
from friendly_traceback.core import FriendlyTraceback
|
||||
|
||||
fr = FriendlyTraceback(type(exc), exc, exc.__traceback__)
|
||||
fr.assign_generic()
|
||||
fr.assign_cause()
|
||||
|
||||
generic = fr.info["generic"] # broad explanation for the exception class
|
||||
cause = fr.info.get("cause") # more specific explanation
|
||||
|
||||
if "https://github.com" in generic:
|
||||
# This is a placeholder message when there is no explanation,
|
||||
# with a suggestion to report the case on GitHub.
|
||||
return ""
|
||||
|
||||
# Add a blank line between the standard message and the friendly message
|
||||
result = "\n\n" + generic
|
||||
|
||||
# Check for the placeholder message again in the cause
|
||||
if cause and "https://github.com" not in cause:
|
||||
result += "\n" + cause
|
||||
|
||||
result = result.rstrip()
|
||||
if isinstance(exc, SyntaxError):
|
||||
result += "\n\n"
|
||||
|
||||
return result
|
||||
except (Exception, SystemExit):
|
||||
# This can go wrong in many ways, it's not worth propagating the error.
|
||||
# friendly-traceback raises SystemExit when it encounters an internal error.
|
||||
# Note that SystemExit is not a subclass of Exception.
|
||||
return ""
|
@ -208,18 +208,9 @@ def _is_special_table(table_id):
|
||||
|
||||
|
||||
def exec_module_text(module_text):
|
||||
mod = imp.new_module(codebuilder.code_filename)
|
||||
codebuilder.save_to_linecache(module_text)
|
||||
code_obj = compile(module_text, codebuilder.code_filename, "exec")
|
||||
# pylint: disable=exec-used
|
||||
filename = "usercode"
|
||||
mod = imp.new_module(filename)
|
||||
|
||||
# Ensure that source lines show in tracebacks
|
||||
linecache.cache[filename] = (
|
||||
len(module_text),
|
||||
None,
|
||||
[line + '\n' for line in module_text.splitlines()],
|
||||
filename,
|
||||
)
|
||||
|
||||
code_obj = compile(module_text, filename, "exec")
|
||||
exec(code_obj, mod.__dict__)
|
||||
return mod
|
||||
|
@ -15,9 +15,12 @@ import traceback
|
||||
from datetime import date, datetime
|
||||
from math import isnan
|
||||
|
||||
import six
|
||||
|
||||
import friendly_errors
|
||||
import moment
|
||||
import records
|
||||
import six
|
||||
import depend
|
||||
|
||||
|
||||
class UnmarshallableError(ValueError):
|
||||
@ -306,6 +309,10 @@ class RaisedException(object):
|
||||
if include_details:
|
||||
self.details = traceback.format_exc()
|
||||
self._message = str(error) + location
|
||||
if not (isinstance(error, (SyntaxError, depend.CircularRefError)) or error != self.error):
|
||||
# For SyntaxError, the friendly message was already added earlier.
|
||||
# CircularRefError and CellError are Grist-specific and have no friendly message.
|
||||
self._message += friendly_errors.friendly_message(error)
|
||||
elif isinstance(error, InvalidTypedValue):
|
||||
self._message = error.typename
|
||||
self.details = error.value
|
||||
|
@ -72,22 +72,44 @@ class TestCodeBuilder(unittest.TestCase):
|
||||
"'''test1'''\nreturn \"\"\"test2\"\"\"")
|
||||
|
||||
# Test that we produce valid code when "$foo" occurs in invalid places.
|
||||
if six.PY2:
|
||||
raise_code = "raise SyntaxError('invalid syntax', ('usercode', 1, 5, u'foo($bar=1)'))"
|
||||
else:
|
||||
raise_code = ("raise SyntaxError('invalid syntax\\n\\n"
|
||||
"A `SyntaxError` occurs when Python cannot understand your code.\\n\\n', "
|
||||
"('usercode', 1, 5, 'foo($bar=1)'))")
|
||||
self.assertEqual(make_body('foo($bar=1)'),
|
||||
"# foo($bar=1)\n"
|
||||
"raise SyntaxError('invalid syntax', ('usercode', 1, 5, %s'foo($bar=1)'))"
|
||||
% unicode_prefix)
|
||||
"# foo($bar=1)\n" + raise_code)
|
||||
|
||||
if six.PY2:
|
||||
raise_code = ("raise SyntaxError('invalid syntax', "
|
||||
"('usercode', 1, 5, u'def $bar(): return 3'))")
|
||||
else:
|
||||
raise_code = ("raise SyntaxError('invalid syntax\\n\\n"
|
||||
"A `SyntaxError` occurs when Python cannot understand your code.\\n\\n', "
|
||||
"('usercode', 1, 5, 'def $bar(): return 3'))")
|
||||
self.assertEqual(make_body('def $bar(): return 3'),
|
||||
"# def $bar(): return 3\n"
|
||||
"raise SyntaxError('invalid syntax', "
|
||||
"('usercode', 1, 5, %s'def $bar(): return 3'))"
|
||||
% unicode_prefix)
|
||||
"# def $bar(): return 3\n" + raise_code)
|
||||
|
||||
# If $ is a syntax error, we don't want to turn it into a different syntax error.
|
||||
if six.PY2:
|
||||
raise_code = ("raise SyntaxError('invalid syntax', "
|
||||
"('usercode', 1, 17, u'$foo + (\"$%.2f\" $ ($17.5))'))")
|
||||
else:
|
||||
raise_code = ("raise SyntaxError('invalid syntax\\n\\n"
|
||||
"A `SyntaxError` occurs when Python cannot understand your code.\\n\\n', "
|
||||
"('usercode', 1, 17, '$foo + (\"$%.2f\" $ ($17.5))'))")
|
||||
self.assertEqual(make_body('$foo + ("$%.2f" $ ($17.5))'),
|
||||
'# $foo + ("$%.2f" $ ($17.5))\n'
|
||||
"raise SyntaxError('invalid syntax', "
|
||||
"('usercode', 1, 17, {}'$foo + (\"$%.2f\" $ ($17.5))'))"
|
||||
.format(unicode_prefix))
|
||||
'# $foo + ("$%.2f" $ ($17.5))\n' + raise_code)
|
||||
|
||||
if six.PY2:
|
||||
raise_code = "raise SyntaxError('invalid syntax', ('usercode', 4, 10, u' return $ bar'))"
|
||||
else:
|
||||
raise_code = ("raise SyntaxError('invalid syntax\\n\\n"
|
||||
"A `SyntaxError` occurs when Python cannot understand your code.\\n\\n"
|
||||
"I am guessing that you wrote `$` by mistake.\\n"
|
||||
"Removing it and writing `return bar` seems to fix the error.\\n\\n', "
|
||||
"('usercode', 4, 10, ' return $ bar'))")
|
||||
self.assertEqual(make_body('if $foo:\n' +
|
||||
' return $foo\n' +
|
||||
'else:\n' +
|
||||
@ -96,8 +118,7 @@ class TestCodeBuilder(unittest.TestCase):
|
||||
'# return $foo\n' +
|
||||
'# else:\n' +
|
||||
'# return $ bar\n' +
|
||||
"raise SyntaxError('invalid syntax', ('usercode', 4, 10, %s' return $ bar'))"
|
||||
% unicode_prefix)
|
||||
raise_code)
|
||||
|
||||
# Check for reasonable behaviour with non-empty text and no statements.
|
||||
self.assertEqual(make_body('# comment'), '# comment\npass')
|
||||
|
@ -270,7 +270,7 @@ class EngineTestCase(unittest.TestCase):
|
||||
def assertFormulaError(self, exc, type_, message, tracebackRegexp=None):
|
||||
self.assertIsInstance(exc, objtypes.RaisedException)
|
||||
self.assertIsInstance(exc.error, type_)
|
||||
self.assertEqual(str(exc.error), message)
|
||||
self.assertEqual(exc._message, message)
|
||||
if tracebackRegexp:
|
||||
self.assertRegex(exc.details, tracebackRegexp)
|
||||
|
||||
|
@ -54,12 +54,33 @@ else:
|
||||
TypeError, 'SQRT() takes exactly 1 argument (2 given)',
|
||||
r"TypeError: SQRT\(\) takes exactly 1 argument \(2 given\)")
|
||||
else:
|
||||
self.assertFormulaError(self.engine.get_formula_error('Math', 'excel_formula', 3),
|
||||
TypeError, 'SQRT() takes 1 positional argument but 2 were given',
|
||||
r"TypeError: SQRT\(\) takes 1 positional argument but 2 were given")
|
||||
self.assertFormulaError(
|
||||
self.engine.get_formula_error('Math', 'excel_formula', 3), TypeError,
|
||||
'SQRT() takes 1 positional argument but 2 were given\n\n'
|
||||
'A `TypeError` is usually caused by trying\n'
|
||||
'to combine two incompatible types of objects,\n'
|
||||
'by calling a function with the wrong type of object,\n'
|
||||
'or by trying to do an operation not allowed on a given type of object.\n\n'
|
||||
'You apparently have called the function `SQRT` with\n'
|
||||
'2 positional argument(s) while it requires 1\n'
|
||||
'such positional argument(s).',
|
||||
r"TypeError: SQRT\(\) takes 1 positional argument but 2 were given",
|
||||
)
|
||||
|
||||
int_not_iterable_message = "'int' object is not iterable"
|
||||
if six.PY3:
|
||||
int_not_iterable_message += (
|
||||
'\n\n'
|
||||
'A `TypeError` is usually caused by trying\n'
|
||||
'to combine two incompatible types of objects,\n'
|
||||
'by calling a function with the wrong type of object,\n'
|
||||
'or by trying to do an operation not allowed on a given type of object.\n\n'
|
||||
'An iterable is an object capable of returning its members one at a time.\n'
|
||||
'Python containers (`list, tuple, dict`, etc.) are iterables.\n'
|
||||
'An iterable is required here.'
|
||||
)
|
||||
self.assertFormulaError(self.engine.get_formula_error('Math', 'built_in_formula', 3),
|
||||
TypeError, "'int' object is not iterable",
|
||||
TypeError, int_not_iterable_message,
|
||||
textwrap.dedent(
|
||||
r"""
|
||||
File "usercode", line \d+, in built_in_formula
|
||||
@ -68,8 +89,21 @@ else:
|
||||
"""
|
||||
))
|
||||
|
||||
if six.PY2:
|
||||
message = "invalid syntax (usercode, line 5)"
|
||||
else:
|
||||
message = textwrap.dedent(
|
||||
"""\
|
||||
invalid syntax
|
||||
|
||||
A `SyntaxError` occurs when Python cannot understand your code.
|
||||
|
||||
I am guessing that you wrote `:` by mistake.
|
||||
Removing it and writing `return 0` seems to fix the error.
|
||||
|
||||
(usercode, line 5)""")
|
||||
self.assertFormulaError(self.engine.get_formula_error('Math', 'syntax_err', 3),
|
||||
SyntaxError, "invalid syntax (usercode, line 5)",
|
||||
SyntaxError, message,
|
||||
textwrap.dedent(
|
||||
r"""
|
||||
File "usercode", line 5
|
||||
@ -88,6 +122,7 @@ else:
|
||||
IndentationError: unexpected indent
|
||||
"""
|
||||
)
|
||||
message = 'unexpected indent (usercode, line 2)'
|
||||
else:
|
||||
traceback_regex = textwrap.dedent(
|
||||
r"""
|
||||
@ -96,12 +131,21 @@ else:
|
||||
IndentationError: unexpected indent
|
||||
"""
|
||||
)
|
||||
message = textwrap.dedent(
|
||||
"""\
|
||||
unexpected indent
|
||||
|
||||
An `IndentationError` occurs when a given line of code is
|
||||
not indented (aligned vertically with other lines) as expected.
|
||||
|
||||
Line `2` identified above is more indented than expected.
|
||||
|
||||
(usercode, line 2)""")
|
||||
self.assertFormulaError(self.engine.get_formula_error('Math', 'indent_err', 3),
|
||||
IndentationError, 'unexpected indent (usercode, line 2)',
|
||||
traceback_regex)
|
||||
IndentationError, message, traceback_regex)
|
||||
|
||||
self.assertFormulaError(self.engine.get_formula_error('Math', 'other_err', 3),
|
||||
TypeError, "'int' object is not iterable",
|
||||
TypeError, int_not_iterable_message,
|
||||
textwrap.dedent(
|
||||
r"""
|
||||
File "usercode", line \d+, in other_err
|
||||
@ -350,9 +394,11 @@ else:
|
||||
AttributeError, "Table 'AttrTest' has no column 'AA'",
|
||||
r"AttributeError: Table 'AttrTest' has no column 'AA'")
|
||||
cell_error = self.engine.get_formula_error('AttrTest', 'C', 1)
|
||||
self.assertFormulaError(cell_error,
|
||||
objtypes.CellError, "AttributeError in referenced cell AttrTest[1].B",
|
||||
r"CellError: AttributeError in referenced cell AttrTest\[1\].B")
|
||||
self.assertFormulaError(
|
||||
cell_error, objtypes.CellError,
|
||||
"Table 'AttrTest' has no column 'AA'\n(in referenced cell AttrTest[1].B)",
|
||||
r"CellError: AttributeError in referenced cell AttrTest\[1\].B",
|
||||
)
|
||||
self.assertEqual(
|
||||
objtypes.encode_object(cell_error),
|
||||
['E',
|
||||
|
@ -71,9 +71,12 @@ class TestGenCode(unittest.TestCase):
|
||||
gcode.make_module(self.schema)
|
||||
generated = gcode.get_user_text()
|
||||
if six.PY3:
|
||||
generated = generated.replace(
|
||||
", 'for a in b'))",
|
||||
", u'for a in b'))",
|
||||
saved_sample = saved_sample.replace(
|
||||
"raise SyntaxError('invalid syntax', ('usercode', 1, 11, u'for a in b'))",
|
||||
"raise SyntaxError('invalid syntax\\n\\n"
|
||||
"A `SyntaxError` occurs when Python cannot understand your code.\\n\\n"
|
||||
"You wrote a `for` loop but\\nforgot to add a colon `:` at the end\\n\\n', "
|
||||
"('usercode', 1, 11, 'for a in b'))"
|
||||
)
|
||||
self.assertEqual(generated, saved_sample, "Generated code doesn't match sample:\n" +
|
||||
"".join(difflib.unified_diff(generated.splitlines(True),
|
||||
|
@ -1,5 +1,8 @@
|
||||
import copy
|
||||
import time
|
||||
|
||||
import six
|
||||
|
||||
import logger
|
||||
import objtypes
|
||||
import testutil
|
||||
@ -669,8 +672,20 @@ class TestTriggerFormulas(test_engine.EngineTestCase):
|
||||
["id", "A", "B", "C"],
|
||||
[1, 0, 0, div_error(0)],
|
||||
])
|
||||
message = 'float division by zero'
|
||||
if six.PY3:
|
||||
message += """
|
||||
|
||||
A `ZeroDivisionError` occurs when you are attempting to divide a value
|
||||
by zero either directly or by using some other mathematical operation.
|
||||
|
||||
You are dividing by the following term
|
||||
|
||||
rec.A
|
||||
|
||||
which is equal to zero."""
|
||||
self.assertFormulaError(self.engine.get_formula_error('Math', 'C', 1),
|
||||
ZeroDivisionError, 'float division by zero',
|
||||
ZeroDivisionError, message,
|
||||
r"1/rec\.A \+ 1/rec\.B")
|
||||
self.update_record("Math", 1, A=1)
|
||||
|
||||
@ -682,7 +697,6 @@ class TestTriggerFormulas(test_engine.EngineTestCase):
|
||||
])
|
||||
error = self.engine.get_formula_error('Math', 'C', 1)
|
||||
self.assertFormulaError(error, ZeroDivisionError, 'float division by zero')
|
||||
self.assertEqual(error._message, 'float division by zero')
|
||||
self.assertEqual(error.details, objtypes.RaisedException(ZeroDivisionError()).no_traceback().details)
|
||||
|
||||
|
||||
|
@ -1,4 +1,13 @@
|
||||
astroid==2.5.7 # this is a difference between python 2 and 3, everything else is same
|
||||
# friendly-traceback and its dependencies, for python 3 only
|
||||
friendly-traceback==0.5.46
|
||||
stack-data==0.3.0
|
||||
executing==0.8.3
|
||||
pure-eval==0.2.2
|
||||
|
||||
# Different astroid version for python 3
|
||||
astroid==2.5.7
|
||||
|
||||
# Everything after this is the same for python 2 and 3
|
||||
asttokens==2.0.5
|
||||
backports.functools-lru-cache==1.6.4
|
||||
chardet==4.0.0
|
||||
|
Loading…
Reference in New Issue
Block a user