From f8a93bc2848eba516213142f4f730a1feda5a39e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 5 Mar 2012 09:55:32 +0100 Subject: [PATCH] [FIX] update py.js for operators &al, implement basic crummy version of relativedelta bzr revid: xmo@openerp.com-20120305085532-0vcz6j0m985gjuz1 --- addons/web/static/lib/py.js/.hg_archival.txt | 6 +- addons/web/static/lib/py.js/README.rst | 18 +- addons/web/static/lib/py.js/TODO.rst | 19 +- addons/web/static/lib/py.js/lib/py.js | 196 +++++++++++++++---- addons/web/static/lib/py.js/test/parser.js | 31 +++ addons/web/static/lib/py.js/test/test.js | 67 ++++++- addons/web/static/src/js/core.js | 128 ++++++++++-- 7 files changed, 401 insertions(+), 64 deletions(-) diff --git a/addons/web/static/lib/py.js/.hg_archival.txt b/addons/web/static/lib/py.js/.hg_archival.txt index d10d8a6b1cc..f5355e28a12 100644 --- a/addons/web/static/lib/py.js/.hg_archival.txt +++ b/addons/web/static/lib/py.js/.hg_archival.txt @@ -1,5 +1,5 @@ repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea -node: 7be96c381d9302b4aafaf5eb5381083b0731a9aa +node: 5adb2d9c89e53a6445e3799f9c4dc9110458c149 branch: default -latesttag: 0.4 -latesttagdistance: 7 +latesttag: 0.5 +latesttagdistance: 9 diff --git a/addons/web/static/lib/py.js/README.rst b/addons/web/static/lib/py.js/README.rst index a5aaa289a46..046db7153d1 100644 --- a/addons/web/static/lib/py.js/README.rst +++ b/addons/web/static/lib/py.js/README.rst @@ -10,6 +10,17 @@ specification document is the `Python 2.7 Expressions spec `_ (along with the lexical analysis part). +Syntax +------ + +* Lambdas and ternaries should be parsed but are not implemented (in + the evaluator) +* Only floats are implemented, ``int`` literals are parsed as floats. +* Octal and hexadecimal literals are not implemented +* Srings are backed by JavaScript strings and probably behave like + ``unicode`` more than like ``str`` +* Slices don't work + Builtins -------- @@ -89,9 +100,10 @@ Collections Abstract Base Classes Hashable are kind-of implemented as well) Numeric type emulation - Basically not implemented, the only part of it which is - implemented is the unary ``-`` (because it's used to create - negative floats, they're parsed as a negated positive number) + Operators are implemented (but not tested), ``abs``, ``divmod`` + and ``pow`` builtins are not implemented yet. Neither are ``oct`` + and ``hex`` but I'm not sure we care (I'm not sure we care about + ``pow`` or even ``divmod`` either, for that matter) Utilities --------- diff --git a/addons/web/static/lib/py.js/TODO.rst b/addons/web/static/lib/py.js/TODO.rst index f80520944d5..a1309bd3fc5 100644 --- a/addons/web/static/lib/py.js/TODO.rst +++ b/addons/web/static/lib/py.js/TODO.rst @@ -34,15 +34,14 @@ Base methods requirement ************************ * ``__getattr__`` -* ? ``__getitem`` -* ``__call__`` -* ``or`` -* ``toJS`` / ``toJSON`` * ``dict.get`` -* ``datetime.time.today`` -* ``datetime.time.strftime`` -* ``time.strftime`` -* ``__add__`` / ``__radd__`` -* ``__sub__`` / ``__rsub__`` * ``__len__`` -* ``__nonzero__`` + +In datamodel, not implemented in any type, untested +*************************************************** + +* a[b] + +* a + b, a - b, a * b, ... + +* +a, ~a diff --git a/addons/web/static/lib/py.js/lib/py.js b/addons/web/static/lib/py.js/lib/py.js index ec6b9182543..3c0b13d0717 100644 --- a/addons/web/static/lib/py.js/lib/py.js +++ b/addons/web/static/lib/py.js/lib/py.js @@ -104,6 +104,15 @@ var py = {}; symbol(':'); symbol(')'); symbol(']'); symbol('}'); symbol(','); symbol('else'); + infix('=', 10, function (left) { + if (left.id !== '(name)') { + throw new Error("Expected keyword argument name, got " + token.id); + } + this.first = left; + this.second = expression(); + return this; + }); + symbol('lambda', 20).nud = function () { this.first = []; if (token.id !== ':') { @@ -362,7 +371,7 @@ var py = {}; if (val instanceof py.object || val === py.object - || py.issubclass.__call__(val, py.object) === py.True) { + || py.issubclass.__call__([val, py.object]) === py.True) { return val; } @@ -443,7 +452,7 @@ var py = {}; // TODO: second argument should be class return val.__get__(this); } - if (typeof val === 'function' && !this.hasOwnProperty(val)) { + if (typeof val === 'function' && !this.hasOwnProperty(name)) { // val is a method from the class return new PY_instancemethod(val, this); } @@ -476,6 +485,7 @@ var py = {}; py.NotImplemented = new NotImplementedType(); var booleans_initialized = false; py.bool = py.type(function bool(value) { + value = (value instanceof Array) ? value[0] : value; // The only actual instance of py.bool should be py.True // and py.False. Return the new instance of py.bool if we // are initializing py.True and py.False, otherwise always @@ -493,7 +503,26 @@ var py = {}; py.False = new py.bool(); booleans_initialized = true; py.float = py.type(function float(value) { - this._value = value; + value = (value instanceof Array) ? value[0] : value; + if (value === undefined) { this._value = 0; return; } + if (value instanceof py.float) { return value; } + if (typeof value === 'number' || value instanceof Number) { + this._value = value; + return; + } + if (typeof value === 'string' || value instanceof String) { + this._value = parseFloat(value); + return; + } + if (value instanceof py.object && '__float__' in value) { + var res = value.__float__(); + if (res instanceof py.float) { + return res; + } + throw new Error('TypeError: __float__ returned non-float (type ' + + res.constructor.name + ')'); + } + throw new Error('TypeError: float() argument must be a string or a number'); }, py.object, { __eq__: function (other) { return this._value === other._value ? py.True : py.False; @@ -514,9 +543,25 @@ var py = {}; if (!(other instanceof py.float)) { return py.NotImplemented; } return this._value >= other._value ? py.True : py.False; }, + __add__: function (other) { + if (!(other instanceof py.float)) { return py.NotImplemented; } + return new py.float(this._value + other._value); + }, __neg__: function () { return new py.float(-this._value); }, + __sub__: function (other) { + if (!(other instanceof py.float)) { return py.NotImplemented; } + return new py.float(this._value - other._value); + }, + __mul__: function (other) { + if (!(other instanceof py.float)) { return py.NotImplemented; } + return new py.float(this._value * other._value); + }, + __div__: function (other) { + if (!(other instanceof py.float)) { return py.NotImplemented; } + return new py.float(this._value / other._value); + }, __nonzero__: function () { return this._value ? py.True : py.False; }, @@ -525,7 +570,17 @@ var py = {}; } }); py.str = py.type(function str(s) { - this._value = s; + s = (s instanceof Array) ? s[0] : s; + if (s === undefined) { this._value = ''; return; } + if (s instanceof py.str) { return s; } + if (typeof s === 'string' || s instanceof String) { + this._value = s; + return; + } + var v = s.__str__(); + if (v instanceof py.str) { return v; } + throw new Error('TypeError: __str__ returned non-string (type ' + + v.constructor.name + ')'); }, py.object, { __eq__: function (other) { if (other instanceof py.str && this._value === other._value) { @@ -549,6 +604,10 @@ var py = {}; if (!(other instanceof py.str)) { return py.NotImplemented; } return this._value >= other._value ? py.True : py.False; }, + __add__: function (other) { + if (!(other instanceof py.str)) { return py.NotImplemented; } + return new py.str(this._value + other._value); + }, __nonzero__: function () { return this._value.length ? py.True : py.False; }, @@ -596,9 +655,9 @@ var py = {}; this._inst = null; this._func = nativefunc; }, py.object, { - __call__: function () { + __call__: function (args, kwargs) { // don't want to rewrite __call__ for instancemethod - return this._func.apply(this._inst, arguments); + return this._func.call(this._inst, args, kwargs); }, toJSON: function () { return this._func; @@ -610,13 +669,69 @@ var py = {}; this._func = nativefunc; }, py.def, {}); - py.issubclass = new py.def(function issubclass(derived, parent) { + py.issubclass = new py.def(function issubclass(args) { + var derived = args[0], parent = args[1]; // still hurts my brain that this can work return derived.prototype instanceof py.object ? py.True : py.False; }); + + // All binary operators with fallbacks, so they can be applied generically + var PY_operators = { + '==': ['eq', 'eq', function (a, b) { return a === b; }], + '!=': ['ne', 'ne', function (a, b) { return a !== b; }], + '<': ['lt', 'gt', function (a, b) {return a.constructor.name < b.constructor.name;}], + '<=': ['le', 'ge', function (a, b) {return a.constructor.name <= b.constructor.name;}], + '>': ['gt', 'lt', function (a, b) {return a.constructor.name > b.constructor.name;}], + '>=': ['ge', 'le', function (a, b) {return a.constructor.name >= b.constructor.name;}], + + '+': ['add', 'radd'], + '-': ['sub', 'rsub'], + '*': ['mul', 'rmul'], + '/': ['div', 'rdiv'], + '//': ['floordiv', 'rfloordiv'], + '%': ['mod', 'rmod'], + '**': ['pow', 'rpow'], + '<<': ['lshift', 'rlshift'], + '>>': ['rshift', 'rrshift'], + '&': ['and', 'rand'], + '^': ['xor', 'rxor'], + '|': ['or', 'ror'] + }; + /** + * Implements operator fallback/reflection. + * + * First two arguments are the objects to apply the operator on, + * in their actual order (ltr). + * + * Third argument is the actual operator. + * + * If the operator methods raise exceptions, those exceptions are + * not intercepted. + */ + var PY_op = function (o1, o2, op) { + var r; + var methods = PY_operators[op]; + var forward = '__' + methods[0] + '__', reverse = '__' + methods[1] + '__'; + var otherwise = methods[2]; + + if (forward in o1 && (r = o1[forward](o2)) !== py.NotImplemented) { + return r; + } + if (reverse in o2 && (r = o2[reverse](o1)) !== py.NotImplemented) { + return r; + } + if (otherwise) { + return PY_ensurepy(otherwise(o1, o2)); + } + throw new Error( + "TypeError: unsupported operand type(s) for " + op + ": '" + + o1.constructor.name + "' and '" + + o2.constructor.name + "'"); + }; + var PY_builtins = { type: py.type, @@ -643,30 +758,16 @@ var py = {}; var evaluate_operator = function (operator, a, b) { var v; switch (operator) { - case '==': return a.__eq__(b); case 'is': return a === b ? py.True : py.False; - case '!=': return a.__ne__(b); case 'is not': return a !== b ? py.True : py.False; - case '<': - v = a.__lt__(b); - if (v !== py.NotImplemented) { return v; } - return PY_ensurepy(a.constructor.name < b.constructor.name); - case '<=': - v = a.__le__(b); - if (v !== py.NotImplemented) { return v; } - return PY_ensurepy(a.constructor.name <= b.constructor.name); - case '>': - v = a.__gt__(b); - if (v !== py.NotImplemented) { return v; } - return PY_ensurepy(a.constructor.name > b.constructor.name); - case '>=': - v = a.__ge__(b); - if (v !== py.NotImplemented) { return v; } - return PY_ensurepy(a.constructor.name >= b.constructor.name); case 'in': return b.__contains__(a); case 'not in': return b.__contains__(a) === py.True ? py.False : py.True; + case '==': case '!=': + case '<': case '<=': + case '>': case '>=': + return PY_op(a, b, operator); } throw new Error('SyntaxError: unknown comparator [[' + operator + ']]'); }; @@ -700,11 +801,6 @@ var py = {}; if (result === py.False) { return py.False; } } return py.True; - case '-': - if (expr.second) { - throw new Error('SyntaxError: binary [-] not implemented yet'); - } - return (py.evaluate(expr.first, context)).__neg__(); case 'not': return py.evaluate(expr.first, context).__nonzero__() === py.True ? py.False @@ -723,12 +819,20 @@ var py = {}; return py.evaluate(expr.second, context); case '(': if (expr.second) { - var callable = py.evaluate(expr.first, context), args=[]; + var callable = py.evaluate(expr.first, context); + var args = [], kwargs = {}; for (var jj=0; jj>': + case '&': case '^': case '|': + return PY_op( + py.evaluate(expr.first, context), + py.evaluate(expr.second, context), + expr.id); + default: throw new Error('SyntaxError: Unknown node [[' + expr.id + ']]'); } diff --git a/addons/web/static/lib/py.js/test/parser.js b/addons/web/static/lib/py.js/test/parser.js index ca74d2bebc8..bbbd5d7663d 100644 --- a/addons/web/static/lib/py.js/test/parser.js +++ b/addons/web/static/lib/py.js/test/parser.js @@ -11,6 +11,14 @@ expect.Assertion.prototype.tokens = function (n) { 'expected ' + this.obj + ' to not have an end token'); }; +expect.Assertion.prototype.named = function (value) { + this.assert(this.obj.id === '(name)', + 'expected ' + this.obj + ' to be a name token', + 'expected ' + this.obj + ' not to be a name token'); + this.assert(this.obj.value === value, + 'expected ' + this.obj + ' to have tokenized ' + value, + 'expected ' + this.obj + ' not to have tokenized ' + value); +}; expect.Assertion.prototype.constant = function (value) { this.assert(this.obj.id === '(constant)', 'expected ' + this.obj + ' to be a constant token', @@ -98,4 +106,27 @@ describe('Tokenizer', function () { expect(toks[1].id).to.be(')'); }); }); + describe('functions', function () { + it('tokenizes kwargs', function () { + var toks = py.tokenize('foo(bar=3, qux=4)'); + expect(toks).to.have.tokens(10); + }); + }); +}); + +describe('Parser', function () { + describe('functions', function () { + var ast = py.parse(py.tokenize('foo(bar=3, qux=4)')); + expect(ast.id).to.be('('); + expect(ast.first).to.be.named('foo'); + + args = ast.second; + expect(args[0].id).to.be('='); + expect(args[0].first).to.be.named('bar'); + expect(args[0].second).to.be.number(3); + + expect(args[1].id).to.be('='); + expect(args[1].first).to.be.named('qux'); + expect(args[1].second).to.be.number(4); + }); }); \ No newline at end of file diff --git a/addons/web/static/lib/py.js/test/test.js b/addons/web/static/lib/py.js/test/test.js index 6c53bcfad85..31fccc23240 100644 --- a/addons/web/static/lib/py.js/test/test.js +++ b/addons/web/static/lib/py.js/test/test.js @@ -262,6 +262,15 @@ describe('Attribute access', function () { }); expect(py.eval('foo.bar()', {foo: o})).to.be('ok'); }); + it('should not convert function attributes into methods', function () { + var o = new py.object(); + o.bar = new py.type(function bar() {}); + o.bar.__getattribute__ = function () { + return o.bar.baz; + } + o.bar.baz = py.True; + expect(py.eval('foo.bar.baz', {foo: o})).to.be(true); + }); it('should work on instance attributes', function () { var typ = py.type(function MyType() { this.attr = new py.float(3); @@ -296,16 +305,37 @@ describe('Callables', function () { }); expect(py.eval('MyType()', {MyType: typ})).to.be(true); }); + it('should accept kwargs', function () { + expect(py.eval('foo(ok=True)', { + foo: function foo() { return py.True; } + })).to.be(true); + }); + it('should be able to get its kwargs', function () { + expect(py.eval('foo(ok=True)', { + foo: function foo(args, kwargs) { return kwargs.ok; } + })).to.be(true); + }); + it('should be able to have both args and kwargs', function () { + expect(py.eval('foo(1, 2, 3, ok=True, nok=False)', { + foo: function (args, kwargs) { + expect(args).to.have.length(3); + expect(args[0].toJSON()).to.be(1); + expect(kwargs).to.only.have.keys('ok', 'nok') + expect(kwargs.nok.toJSON()).to.be(false); + return kwargs.ok; + } + })).to.be(true); + }); }); describe('issubclass', function () { it('should say a type is its own subclass', function () { - expect(py.issubclass.__call__(py.dict, py.dict).toJSON()) + expect(py.issubclass.__call__([py.dict, py.dict]).toJSON()) .to.be(true); expect(py.eval('issubclass(dict, dict)')) .to.be(true); }); it('should work with subtypes', function () { - expect(py.issubclass.__call__(py.bool, py.object).toJSON()) + expect(py.issubclass.__call__([py.bool, py.object]).toJSON()) .to.be(true); }); }); @@ -313,4 +343,37 @@ describe('builtins', function () { it('should aways be available', function () { expect(py.eval('bool("foo")')).to.be(true); }); +}); + +describe('numerical protocols', function () { + describe('True numbers (float)', function () { + describe('Basic arithmetic', function () { + it('can be added', function () { + expect(py.eval('1 + 1')).to.be(2); + expect(py.eval('1.5 + 2')).to.be(3.5); + expect(py.eval('1 + -1')).to.be(0); + }); + it('can be subtracted', function () { + expect(py.eval('1 - 1')).to.be(0); + expect(py.eval('1.5 - 2')).to.be(-0.5); + expect(py.eval('2 - 1.5')).to.be(0.5); + }); + it('can be multiplied', function () { + expect(py.eval('1 * 3')).to.be(3); + expect(py.eval('0 * 5')).to.be(0); + expect(py.eval('42 * -2')).to.be(-84); + }); + it('can be divided', function () { + expect(py.eval('1 / 2')).to.be(0.5); + expect(py.eval('2 / 1')).to.be(2); + }); + }); + }); + describe('Strings', function () { + describe('Basic arithmetics operators', function () { + it('can be added (concatenation)', function () { + expect(py.eval('"foo" + "bar"')).to.be('foobar'); + }); + }); + }); }); \ No newline at end of file diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js index ca53bce9c59..ead33822926 100644 --- a/addons/web/static/src/js/core.js +++ b/addons/web/static/src/js/core.js @@ -322,19 +322,34 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. return this.session_init(); }, test_eval_get_context: function () { + var asJS = function (arg) { + if (arg instanceof py.object) { + return arg.toJSON(); + } + return arg; + }; + var datetime = new py.object(); + datetime.datetime = new py.type(function datetime() { + throw new Error('datetime.datetime not implemented'); + }); var date = datetime.date = new py.type(function date(y, m, d) { - this._year = y; - this._month = m; - this._day = d; + if (y instanceof Array) { + d = y[2]; + m = y[1]; + y = y[0]; + } + this.year = asJS(y); + this.month = asJS(m); + this.day = asJS(d); }, py.object, { - strftime: function (format) { - var f = format.toJSON(), self = this; + strftime: function (args) { + var f = asJS(args[0]), self = this; return new py.str(f.replace(/%([A-Za-z])/g, function (m, c) { switch (c) { - case 'Y': return self._year; - case 'm': return _.str.sprintf('%02d', self._month); - case 'd': return _.str.sprintf('%02d', self._day); + case 'Y': return self.year; + case 'm': return _.str.sprintf('%02d', self.month); + case 'd': return _.str.sprintf('%02d', self.day); } throw new Error('ValueError: No known conversion for ' + m); })); @@ -350,16 +365,107 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. var d = new Date(); return new date(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()); }); + datetime.time = new py.type(function time() { + throw new Error('datetime.time not implemented'); + }); var time = new py.object(); - time.strftime = new py.def(function (format) { - return date.today.__call__().strftime(format); + time.strftime = new py.def(function (args) { + return date.today.__call__().strftime(args); + }); + + var relativedelta = new py.type(function relativedelta(args, kwargs) { + if (!_.isEmpty(args)) { + throw new Error('Extraction of relative deltas from existing datetimes not supported'); + } + this.ops = kwargs; + }, py.object, { + __add__: function (other) { + if (!(other instanceof datetime.date)) { + return py.NotImplemented; + } + // TODO: test this whole mess + var year = asJS(this.ops.year) || asJS(other.year); + if (asJS(this.ops.years)) { + year += asJS(this.ops.years); + } + + var month = asJS(this.ops.month) || asJS(other.month); + if (asJS(this.ops.months)) { + month += asJS(this.ops.months); + // FIXME: no divmod in JS? + while (month < 1) { + year -= 1; + month += 12; + } + while (month > 12) { + year += 1; + month -= 12; + } + } + + var lastMonthDay = new Date(year, month, 0).getDate(); + var day = asJS(this.ops.day) || asJS(other.day); + if (day > lastMonthDay) { day = lastMonthDay; } + var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0); + if (days_offset) { + day = new Date(year, month-1, day + days_offset).getDate(); + } + // TODO: leapdays? + // TODO: hours, minutes, seconds? Not used in XML domains + // TODO: weekday? + return new datetime.date(year, month, day); + }, + __radd__: function (other) { + return this.__add__(other); + }, + + __sub__: function (other) { + if (!(other instanceof datetime.date)) { + return py.NotImplemented; + } + // TODO: test this whole mess + var year = asJS(this.ops.year) || asJS(other.year); + if (asJS(this.ops.years)) { + year -= asJS(this.ops.years); + } + + var month = asJS(this.ops.month) || asJS(other.month); + if (asJS(this.ops.months)) { + month -= asJS(this.ops.months); + // FIXME: no divmod in JS? + while (month < 1) { + year -= 1; + month += 12; + } + while (month > 12) { + year += 1; + month -= 12; + } + } + + var lastMonthDay = new Date(year, month, 0).getDate(); + var day = asJS(this.ops.day) || asJS(other.day); + if (day > lastMonthDay) { day = lastMonthDay; } + var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0); + if (days_offset) { + day = new Date(year, month-1, day - days_offset).getDate(); + } + // TODO: leapdays? + // TODO: hours, minutes, seconds? Not used in XML domains + // TODO: weekday? + return new datetime.date(year, month, day); + }, + __rsub__: function (other) { + return this.__sub__(other); + } }); return { uid: new py.float(this.uid), datetime: datetime, - time: time + time: time, + relativedelta: relativedelta }; }, test_eval: function (source, expected) {