mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
b4cc519616
Summary: This addresses two issues, differently: - For a formula with leading whitespace, like " 1+1", it is stored as is, but is fixed to work (it should be valid Python, and whitespace is only stripped out at parsing time to avoid intentation errors caused by the way it gets parsed) - For a formula with a leading equals-sign ("="), it is stripped out on the client side before the formula is stored. Grist documentation uses leading "=" to indicate formulas (because UI shows an "=" icon), and Excel formulas actually contain the leading "=", so it is a common mistake to include it. Test Plan: Added new test cases Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3873
257 lines
11 KiB
Python
257 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
import codebuilder
|
|
import six
|
|
import test_engine
|
|
|
|
unicode_prefix = 'u' if six.PY2 else ''
|
|
|
|
def make_body(formula, default=None):
|
|
return codebuilder.make_formula_body(formula, default).get_text()
|
|
|
|
class TestCodeBuilder(test_engine.EngineTestCase):
|
|
def test_make_formula_body(self):
|
|
# Test simple usage.
|
|
self.assertEqual(make_body(""), "return None")
|
|
self.assertEqual(make_body("", 0.0), "return 0.0")
|
|
self.assertEqual(make_body("", ""), "return ''")
|
|
self.assertEqual(make_body(" "), "return None")
|
|
self.assertEqual(make_body(" ", "-"), "return '-'")
|
|
self.assertEqual(make_body("\n\t"), "return None")
|
|
self.assertEqual(make_body("$foo"), "return rec.foo")
|
|
self.assertEqual(make_body("rec.foo"), "return rec.foo")
|
|
self.assertEqual(make_body("return $foo"), "return rec.foo")
|
|
self.assertEqual(make_body("return $f123"), "return rec.f123")
|
|
self.assertEqual(make_body("return rec.foo"), "return rec.foo")
|
|
self.assertEqual(make_body("$foo if $bar else max($foo.bar.baz)"),
|
|
"return rec.foo if rec.bar else max(rec.foo.bar.baz)")
|
|
|
|
# Check that we don't mistake our temporary representation of "$" for the real thing.
|
|
self.assertEqual(make_body("return DOLLARfoo"), "return DOLLARfoo")
|
|
|
|
# Test that we don't translate $foo inside string literals or comments.
|
|
self.assertEqual(make_body("$foo or '$foo'"), "return rec.foo or '$foo'")
|
|
self.assertEqual(make_body("$foo * 2 # $foo"), "return rec.foo * 2 # $foo")
|
|
self.assertEqual(make_body("$foo * 2 # $foo\n$bar"), "rec.foo * 2 # $foo\nreturn rec.bar")
|
|
self.assertEqual(make_body("$foo or '\\'$foo\\''"), "return rec.foo or '\\'$foo\\''")
|
|
self.assertEqual(make_body('$foo or """$foo"""'), 'return rec.foo or """$foo"""')
|
|
self.assertEqual(make_body('$foo or """Some "$foos" stay"""'),
|
|
'return rec.foo or """Some "$foos" stay"""')
|
|
|
|
# Check that we only insert a return appropriately.
|
|
self.assertEqual(make_body('if $foo:\n return 1\nelse:\n return 2\n'),
|
|
'if rec.foo:\n return 1\nelse:\n return 2\n')
|
|
self.assertEqual(make_body('a = $foo\nmax(a, a*2)'), 'a = rec.foo\nreturn max(a, a*2)')
|
|
|
|
# Check that return gets inserted correctly when there is a multi-line expression.
|
|
self.assertEqual(make_body('($foo or\n $bar)'), 'return (rec.foo or\n rec.bar)')
|
|
self.assertEqual(make_body('return ($foo or\n $bar)'), 'return (rec.foo or\n rec.bar)')
|
|
self.assertEqual(make_body('if $foo: return 17'), 'if rec.foo: return 17')
|
|
self.assertEqual(make_body('$foo\n# return $bar'), 'return rec.foo\n# return $bar')
|
|
|
|
# Test that formulas with a single string literal work, including multi-line string literals.
|
|
self.assertEqual(make_body('"test"'), 'return "test"')
|
|
self.assertEqual(make_body('("""test1\ntest2\ntest3""")'), 'return ("""test1\ntest2\ntest3""")')
|
|
self.assertEqual(make_body('"""test1\ntest2\ntest3"""'), 'return """test1\ntest2\ntest3"""')
|
|
self.assertEqual(make_body('"""test1\\ntest2\\ntest3"""'), 'return """test1\\ntest2\\ntest3"""')
|
|
|
|
# Same, with single quotes.
|
|
self.assertEqual(make_body("'test'"), "return 'test'")
|
|
self.assertEqual(make_body("('''test1\ntest2\ntest3''')"), "return ('''test1\ntest2\ntest3''')")
|
|
self.assertEqual(make_body("'''test1\ntest2\ntest3'''"), "return '''test1\ntest2\ntest3'''")
|
|
self.assertEqual(make_body("'''test1\\ntest2\\ntest3'''"), "return '''test1\\ntest2\\ntest3'''")
|
|
|
|
# And with mixing quotes
|
|
self.assertEqual(make_body("'''test1\"\"\" +\\\n \"\"\"test2'''"),
|
|
"return '''test1\"\"\" +\\\n \"\"\"test2'''")
|
|
self.assertEqual(make_body("'''test1''' +\\\n \"\"\"test2\"\"\""),
|
|
"return '''test1''' +\\\n \"\"\"test2\"\"\"")
|
|
self.assertEqual(make_body("'''test1\"\"\"\n\"\"\"test2'''"),
|
|
"return '''test1\"\"\"\n\"\"\"test2'''")
|
|
self.assertEqual(make_body("'''test1'''\n\"\"\"test2\"\"\""),
|
|
"'''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_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_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_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' +
|
|
' return $ bar\n'),
|
|
'# if $foo:\n' +
|
|
'# return $foo\n' +
|
|
'# else:\n' +
|
|
'# return $ bar\n' +
|
|
raise_code)
|
|
|
|
# Check for reasonable behaviour with non-empty text and no statements.
|
|
self.assertEqual(make_body('# comment'), '# comment\npass')
|
|
|
|
self.assertEqual(make_body('rec = 1; rec'), "# rec = 1; rec\n" +
|
|
"raise SyntaxError('Grist disallows assignment " +
|
|
"to the special variable \"rec\"', ('usercode', 1, 1, %s'rec = 1; rec'))"
|
|
% unicode_prefix)
|
|
self.assertEqual(make_body('for rec in []: return rec'), "# for rec in []: return rec\n" +
|
|
"raise SyntaxError('Grist disallows assignment " +
|
|
"to the special variable \"rec\"', "
|
|
"('usercode', 1, 4, %s'for rec in []: return rec'))"
|
|
% unicode_prefix)
|
|
|
|
# some legitimates use of rec
|
|
body = ("""
|
|
foo = rec
|
|
[rec for x in rec]
|
|
for a in rec:
|
|
t = a
|
|
[rec for x in rec]
|
|
return rec
|
|
""")
|
|
self.assertEqual(make_body(body), body)
|
|
|
|
# mostly legitimate use of rec but one failing
|
|
body = ("""
|
|
foo = rec
|
|
[1 for rec in []]
|
|
for a in rec:
|
|
t = a
|
|
[rec for x in rec]
|
|
return rec
|
|
""")
|
|
|
|
self.assertRegex(make_body(body),
|
|
r"raise SyntaxError\('Grist disallows assignment" +
|
|
r" to the special variable \"rec\"', "
|
|
r"\('usercode', 3, 7, %s'\[1 for rec in \[\]\]'\)\)"
|
|
% unicode_prefix)
|
|
|
|
self.assertEqual(make_body('rec.foo = 1; rec'), "# rec.foo = 1; rec\n" +
|
|
"raise SyntaxError(\"You can't assign a value to a column with `=`. "
|
|
"If you mean to check for equality, use `==` instead.\", "
|
|
"('usercode', 1, 1, %s'rec.foo = 1; rec'))"
|
|
% unicode_prefix)
|
|
|
|
self.assertEqual(make_body('$foo = 1; rec'), "# $foo = 1; rec\n" +
|
|
"raise SyntaxError(\"You can't assign a value to a column with `=`. "
|
|
"If you mean to check for equality, use `==` instead.\", "
|
|
"('usercode', 1, 1, %s'$foo = 1; rec'))"
|
|
% unicode_prefix)
|
|
|
|
self.assertEqual(make_body('assert foo'), "# assert foo\n" +
|
|
'raise SyntaxError("No `return` statement, '
|
|
"and the last line isn't an expression.\", "
|
|
"('usercode', 1, 1, %s'assert foo'))"
|
|
% unicode_prefix)
|
|
|
|
self.assertEqual(make_body('foo = 1'), "# foo = 1\n" +
|
|
'raise SyntaxError("No `return` statement, '
|
|
"and the last line isn't an expression."
|
|
" If you want to check for equality, use `==` instead of `=`.\", "
|
|
"('usercode', 1, 1, %s'foo = 1'))"
|
|
% unicode_prefix)
|
|
|
|
def test_make_formula_body_unicode(self):
|
|
# Test that we don't fail when strings include unicode characters
|
|
self.assertEqual(make_body("'résumé' + $foo"), u"return 'résumé' + rec.foo")
|
|
|
|
# Or when a unicode object is passed in, rather than a byte string
|
|
self.assertEqual(make_body(u"'résumé' + $foo"), u"return 'résumé' + rec.foo")
|
|
|
|
# Check the return type of make_body()
|
|
self.assertEqual(type(make_body("foo")), six.text_type)
|
|
self.assertEqual(type(make_body(u"foo")), six.text_type)
|
|
|
|
|
|
def test_wrap_logical(self):
|
|
self.assertEqual(make_body("IF($foo, $bar, $baz)"),
|
|
"return IF(rec.foo, lambda: (rec.bar), lambda: (rec.baz))")
|
|
self.assertEqual(make_body("return IF(FOO(x,y), BAR(x,y) * 2, BAZ(x,y) + 5)"),
|
|
"return IF(FOO(x,y), lambda: (BAR(x,y) * 2), lambda: (BAZ(x,y) + 5))")
|
|
self.assertEqual(make_body("""
|
|
y = $Test
|
|
x = IF( FOO(x,y) or 6,
|
|
BAR($x,y).blahh ,
|
|
Foo.lookupRecords(foo=$foo.bar,
|
|
bar=True
|
|
).baz
|
|
)
|
|
return x or y
|
|
"""), """
|
|
y = rec.Test
|
|
x = IF( FOO(x,y) or 6,
|
|
lambda: (BAR(rec.x,y).blahh) ,
|
|
lambda: (Foo.lookupRecords(foo=rec.foo.bar,
|
|
bar=True
|
|
).baz)
|
|
)
|
|
return x or y
|
|
""")
|
|
self.assertEqual(make_body("IF($A == 0, IF($B > 5, 'Test1'), IF($C < 10, 'Test2', 'Test3'))"),
|
|
"return IF(rec.A == 0, " +
|
|
"lambda: (IF(rec.B > 5, lambda: ('Test1'))), " +
|
|
"lambda: (IF(rec.C < 10, lambda: ('Test2'), lambda: ('Test3'))))"
|
|
)
|
|
|
|
def test_wrap_error(self):
|
|
self.assertEqual(make_body("ISERR($foo.bar)"), "return ISERR(lambda: (rec.foo.bar))")
|
|
self.assertEqual(make_body("ISERROR(1 / 0)"), "return ISERROR(lambda: (1 / 0))")
|
|
self.assertEqual(make_body("IFERROR($foo + #\n 1 / 0, 'XX')"),
|
|
"return IFERROR(lambda: (rec.foo + #\n 1 / 0), 'XX')")
|
|
|
|
# Check that extra parentheses are OK.
|
|
self.assertEqual(make_body("IFERROR((($foo + 1) / 0))"),
|
|
"return IFERROR((lambda: ((rec.foo + 1) / 0)))")
|
|
|
|
# Check that missing arguments is OK
|
|
self.assertEqual(make_body("ISERR()"), "return ISERR()")
|
|
|
|
|
|
def test_leading_whitespace(self):
|
|
self.assertEqual(make_body(" $A + 1"), "return rec.A + 1")
|
|
|
|
self.assertEqual(make_body("""
|
|
if $A:
|
|
return $A
|
|
|
|
$B
|
|
"""), """
|
|
if rec.A:
|
|
return rec.A
|
|
|
|
return rec.B
|
|
""")
|