Skip to content
Snippets Groups Projects
Commit 3248498f authored by Géry Debongnie's avatar Géry Debongnie Committed by Adrien Dieudonne
Browse files

[IMP] web: implement formatAST utility function

In this commit, we implement a function which format a py.js abstract
syntax tree into a string.  This is really useful whenever we need to
manipulate python expressions.
parent eed4d586
No related branches found
No related tags found
No related merge requests found
......@@ -314,6 +314,98 @@ function py_eval(expr, context) {
return py.eval(expr, _.extend({}, context || {}, {"true": true, "false": false, "null": null}));
}
// Binding power for prefix operator is not accessible in the AST generated by
// py.js, so we have to hardcode some values here
var BINDING_POWERS = {
or: 30,
and: 40,
not: 50,
};
/**
* Convert a python AST (generated by py.js) to a string form, which should
* represent the same AST.
*
* @param {Object} ast a valid AST obtained by py.js, which represent a python
* expression
* @param {integer} [lbp=0] a binding power. This is necessary to be able to
* format sub expressions: the + sub expression in "3 * (a + 2)" should be
* formatted with parenthesis, because its binding power is lower than the
* binding power of *.
* @returns {string}
*/
function formatAST(ast, lbp) {
lbp = lbp || 0;
switch (ast.id) {
// basic values
case "(number)":
return String(ast.value);
case "(string)":
return "'" + ast.value + "'";
case "(constant)":
return ast.value;
case "(name)":
return ast.value;
case "[":
if (ast.second) {
// read a value in a dictionary: d['a']
return formatAST(ast.first) + '[' + formatAST(ast.second) + ']';
} else {
// list: [1, 2]
var values = ast.first.map(formatAST);
return '[' + values.join(', ') + ']';
}
case "{":
var keyValues = ast.first.map(function (kv) {
return formatAST(kv[0]) + ': ' + formatAST(kv[1]);
});
return '{' + keyValues.join(', ') + '}';
// relations
case "=":
return formatAST(ast.first) + ' ' + ast.id + ' ' + formatAST(ast.second);
// operators
case "-":
case "+":
case "~":
case "*":
case "**":
case "%":
case "//":
case "and":
case "or":
if (ast.second) {
// infix
var r = formatAST(ast.first, ast.lbp) + ' ' + ast.id + ' ' + formatAST(ast.second, ast.lbp);
if (ast.lbp < lbp) {
r = '(' + r + ')';
}
return r;
}
// prefix
// real lbp is not accessible, it is inside a closure
var actualBP = BINDING_POWERS[ast.id] || 130;
return ast.id + formatAST(ast.first, actualBP);
case ".":
return formatAST(ast.first, ast.lbp) + '.' + formatAST(ast.second);
case "not":
return "not " + formatAST(ast.first);
case "(comparator)":
var operator = ast.operators[0]
return formatAST(ast.expressions[0]) + ' ' + operator + ' ' + formatAST(ast.expressions[1]);
// function call
case "(":
if (ast.second) {
// this is a function call: f(a, b)
return formatAST(ast.first) + '(' + ast.second.map(formatAST).join(', ') + ')';
} else {
// this is a tuple
return '(' + ast.first.map(formatAST).join(', ') + ')';
}
}
throw new Error("Unimplemented python construct");
}
return {
context: pycontext,
......@@ -321,6 +413,7 @@ return {
eval: pyeval,
eval_domains_and_contexts: eval_domains_and_contexts,
py_eval: py_eval,
formatAST: formatAST,
};
});
......@@ -5,9 +5,21 @@ var Context = require('web.Context');
var pyUtils = require('web.py_utils');
var time = require('web.time');
QUnit.assert.checkAST = function (expr, message) {
var ast = py.parse(py.tokenize(expr));
var formattedAST = pyUtils.formatAST(ast);
this.pushResult({
result: expr === formattedAST,
actual: formattedAST,
expected: expr,
message: message
});
};
QUnit.module('core', function () {
QUnit.module('PY.eval');
QUnit.module('py_utils');
QUnit.test('simple python expression', function(assert) {
assert.expect(2);
......@@ -419,7 +431,7 @@ QUnit.module('core', function () {
assert.strictEqual(result, "2012-02-15 00:00:00");
});
QUnit.module('pyeval (eval domain contexts)', {
QUnit.module('py_utils (eval domain contexts)', {
beforeEach: function() {
this.user_context = {
uid: 1,
......@@ -696,7 +708,7 @@ QUnit.module('core', function () {
});
});
QUnit.module('pyeval (contexts)');
QUnit.module('py_utils (contexts)');
QUnit.test('context_recursive', function (assert) {
assert.expect(3);
......@@ -915,7 +927,7 @@ QUnit.module('core', function () {
});
});
QUnit.module('pyeval (domains)');
QUnit.module('py_utils (domains)');
QUnit.test('current_date', function (assert) {
assert.expect(1);
......@@ -948,7 +960,7 @@ QUnit.module('core', function () {
[['foo', '=', 'qux'], ['bar', '>=', 42]]);
});
QUnit.module('pyeval (groupbys)');
QUnit.module('py_utils (groupbys)');
QUnit.test('groupbys_00', function (assert) {
assert.expect(1);
......@@ -1033,6 +1045,120 @@ QUnit.module('core', function () {
assert.deepEqual(result, ['foo', 'bar', 'grault']);
});
QUnit.module('pyutils (formatAST)');
QUnit.test("basic values", function (assert) {
assert.expect(6);
assert.checkAST("1", "integer value");
assert.checkAST("1.4", "float value");
assert.checkAST("-12", "negative integer value");
assert.checkAST("True", "boolean");
assert.checkAST("'some string'", "a string");
assert.checkAST("None", "None");
});
QUnit.test("dictionary", function (assert) {
assert.expect(3);
assert.checkAST("{}", "empty dictionary");
assert.checkAST("{'a': 1}", "dictionary with a single key");
assert.checkAST("d['a']", "get a value in a dictionary");
});
QUnit.test("list", function (assert) {
assert.expect(2);
assert.checkAST("[]", "empty list");
assert.checkAST("[1]", "list with one value");
});
QUnit.test("tuple", function (assert) {
assert.expect(2);
assert.checkAST("()", "empty tuple");
assert.checkAST("(1, 2)", "basic tuple");
});
QUnit.test("simple arithmetic", function (assert) {
assert.expect(15);
assert.checkAST("1 + 2", "addition");
assert.checkAST("+(1 + 2)", "other addition, prefix");
assert.checkAST("1 - 2", "substraction");
assert.checkAST("-1 - 2", "other substraction");
assert.checkAST("-(1 + 2)", "other substraction");
assert.checkAST("1 + 2 + 3", "addition of 3 integers");
assert.checkAST("a + b", "addition of two variables");
assert.checkAST("42 % 5", "modulo operator");
assert.checkAST("a * 10", "multiplication");
assert.checkAST("a ** 10", "**");
assert.checkAST("~10", "bitwise not");
assert.checkAST("~(10 + 3)", "bitwise not");
assert.checkAST("a * (1 + 2)", "multiplication and addition");
assert.checkAST("(a + b) * 43", "addition and multiplication");
assert.checkAST("a // 10", "integer division");
});
QUnit.test("boolean operators", function (assert) {
assert.expect(6);
assert.checkAST("True and False", "boolean operator");
assert.checkAST("True or False", "boolean operator or");
assert.checkAST("(True or False) and False", "boolean operators and and or");
assert.checkAST("not False", "not prefix");
assert.checkAST("not foo", "not prefix with variable");
assert.checkAST("not a in b", "not prefix with expression");
});
QUnit.test("other operators", function (assert) {
assert.expect(7);
assert.checkAST("x == y", "== operator");
assert.checkAST("x != y", "!= operator");
assert.checkAST("x < y", "< operator");
assert.checkAST("x is y", "is operator");
assert.checkAST("x is not y", "is and not operator");
assert.checkAST("x in y", "in operator");
assert.checkAST("x not in y", "not in operator");
});
QUnit.test("equality", function (assert) {
assert.expect(1);
assert.checkAST("a == b", "simple equality");
});
QUnit.test("strftime", function (assert) {
assert.expect(3);
assert.checkAST("time.strftime('%Y')", "strftime with year");
assert.checkAST("time.strftime('%Y') + '-01-30'", "strftime with year");
assert.checkAST("time.strftime('%Y-%m-%d %H:%M:%S')", "strftime with year");
});
QUnit.test("context_today", function (assert) {
assert.expect(1);
assert.checkAST("context_today().strftime('%Y-%m-%d')", "context today call");
});
QUnit.test("function call", function (assert) {
assert.expect(5);
assert.checkAST("td()", "simple call");
assert.checkAST("td(a, b, c)", "simple call with args");
assert.checkAST('td(days = 1)', "simple call with kwargs");
assert.checkAST('f(1, 2, days = 1)', "mixing args and kwargs");
assert.checkAST('str(td(2))', "function call in function call");
});
QUnit.test("various expressions", function (assert) {
assert.expect(3);
assert.checkAST('(a - b).days', "substraction and .days");
assert.checkAST('a + day == date(2002, 3, 3)');
var expr = "[('type', '=', 'in'), ('day', '<=', time.strftime('%Y-%m-%d')), ('day', '>', (context_today() - datetime.timedelta(days = 15)).strftime('%Y-%m-%d'))]";
assert.checkAST(expr);
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment