[FIX] web: add context in pyeval context

When the owl refactoring landed, we rewrote the pyjs evaluation system.
Then, we used the new system in the new code. In particular, the action
service uses it to evaluate the action domain.

But the previous code had a subtle behaviour in the special case of
evaluating domain: it added the context as a pydict object in itself, so
one could evaluate expression such as `context.get('a', 14)` to read the
value of a key in the context with a fallback.

With this commit, we uniformize this behaviour across all python
expression: the `context` key is now reserved, and used to access
anything in the current evaluation context.

closes odoo/odoo#73227

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Géry Debongnie
2021-07-05 12:53:44 +00:00
parent 9583c74e5f
commit 2063b26b09
3 changed files with 24 additions and 13 deletions
+2 -2
View File
@@ -29,10 +29,10 @@ export function parseExpr(expr) {
* Evaluates a python expression
*
* @param {string} expr
* @param {Object} context
* @param {Object} [context]
* @returns {any}
*/
export function evaluateExpr(expr, context) {
export function evaluateExpr(expr, context = {}) {
const ast = parseExpr(expr);
return evaluate(ast, context);
}
@@ -2,7 +2,7 @@
import { BUILTINS } from "./py_builtin";
import { PyDate, PyDateTime, PyRelativeDelta, PyTime } from "./py_date";
import { parseArgs, PY_DICT } from "./py_utils";
import { parseArgs, PY_DICT, toPyDict } from "./py_utils";
// -----------------------------------------------------------------------------
// Types
@@ -199,6 +199,16 @@ const DICT = {
*/
export function evaluate(ast, context = {}) {
const dicts = new Set();
let pyContext;
const evalContext = Object.create(context);
Object.defineProperty(evalContext, "context", {
get() {
if (!pyContext) {
pyContext = toPyDict(context);
}
return pyContext;
},
});
/**
* @param {AST} ast
@@ -209,8 +219,8 @@ export function evaluate(ast, context = {}) {
case 1 /* String */:
return ast.value;
case 5 /* Name */:
if (ast.value in context) {
return context[ast.value];
if (ast.value in evalContext) {
return evalContext[ast.value];
} else if (ast.value in BUILTINS) {
return BUILTINS[ast.value];
} else {
@@ -221,9 +231,9 @@ export function evaluate(ast, context = {}) {
case 2 /* Boolean */:
return ast.value;
case 6 /* UnaryOperator */:
return applyUnaryOp(ast, context);
return applyUnaryOp(ast, evalContext);
case 7 /* BinaryOperator */:
return applyBinaryOp(ast, context);
return applyBinaryOp(ast, evalContext);
case 14 /* BooleanOperator */:
const left = _evaluate(ast.left);
if (ast.op === "and") {
@@ -1,7 +1,6 @@
/** @odoo-module **/
import { evaluateExpr } from "@web/core/py_js/py";
import { toPyDict } from "@web/core/py_js/py_utils";
QUnit.module("py", {}, () => {
QUnit.module("interpreter", () => {
@@ -138,11 +137,13 @@ QUnit.module("py", {}, () => {
assert.deepEqual(evaluateExpr("foo", { foo: [1, 2, 3] }), [1, 2, 3]);
});
QUnit.test("python values in context", (assert) => {
const context = toPyDict({ b: 3 });
assert.strictEqual(evaluateExpr("context.get('b', 54)", { context }), 3);
assert.strictEqual(evaluateExpr("context.get('c', 54)", { context }), 54);
});
QUnit.test(
"special case for context: the eval context can be accessed as 'context'",
(assert) => {
assert.strictEqual(evaluateExpr("context.get('b', 54)", { b: 3 }), 3);
assert.strictEqual(evaluateExpr("context.get('c', 54)", { b: 3 }), 54);
}
);
QUnit.test("throw error if name is not defined", (assert) => {
assert.throws(() => evaluateExpr("a"));