[FIX] update py.js for operators &al, implement basic crummy version of relativedelta

bzr revid: xmo@openerp.com-20120305085532-0vcz6j0m985gjuz1
This commit is contained in:
Xavier Morel 2012-03-05 09:55:32 +01:00
parent 12358af73b
commit f8a93bc284
7 changed files with 401 additions and 64 deletions

View File

@ -1,5 +1,5 @@
repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea
node: 7be96c381d9302b4aafaf5eb5381083b0731a9aa
node: 5adb2d9c89e53a6445e3799f9c4dc9110458c149
branch: default
latesttag: 0.4
latesttagdistance: 7
latesttag: 0.5
latesttagdistance: 9

View File

@ -10,6 +10,17 @@ specification document is the `Python 2.7 Expressions spec
<http://docs.python.org/reference/expressions.html>`_ (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
---------

View File

@ -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

View File

@ -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<expr.second.length; ++jj) {
args.push(py.evaluate(
expr.second[jj], context));
var arg = expr.second[jj];
if (arg.id !== '=') {
// arg
args.push(py.evaluate(arg, context));
} else {
// kwarg
kwargs[arg.first.value] =
py.evaluate(arg.second, context);
}
}
return callable.__call__.apply(callable, args);
return callable.__call__(args, kwargs);
}
var tuple_exprs = expr.first,
tuple_values = [];
@ -741,7 +845,8 @@ var py = {};
return t;
case '[':
if (expr.second) {
throw new Error('SyntaxError: indexing not implemented yet');
return py.evaluate(expr.first, context)
.__getitem__(expr.evaluate(expr.second, context));
}
var list_exprs = expr.first, list_values = [];
for (var k=0; k<list_exprs.length; ++k) {
@ -765,6 +870,27 @@ var py = {};
}
return py.evaluate(expr.first, context)
.__getattribute__(expr.second.value);
// numerical operators
case '~':
return (py.evaluate(expr.first, context)).__invert__();
case '+':
if (!expr.second) {
return (py.evaluate(expr.first, context)).__pos__();
}
case '-':
if (!expr.second) {
return (py.evaluate(expr.first, context)).__neg__();
}
case '*': case '/': case '//':
case '%':
case '**':
case '<<': case '>>':
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 + ']]');
}

View File

@ -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);
});
});

View File

@ -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');
});
});
});
});

View File

@ -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) {