diff --git a/setup/extensions.py b/setup/extensions.py index e2e1549cf7..53dfce3bcb 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -67,7 +67,7 @@ if iswindows: extensions = [ Extension('dukpy', - ['duktape/%s.c' % x for x in 'context conversions proxy module duktape/duktape'.split()], + ['duktape/%s.c' % x for x in 'errors context conversions proxy module duktape/duktape'.split()], headers=['duktape/dukpy.h', 'duktape/duktape/duktape.h'], optimize_level=2, ), diff --git a/src/duktape/__init__.py b/src/duktape/__init__.py index 9218222fb2..d4a64c5f78 100644 --- a/src/duktape/__init__.py +++ b/src/duktape/__init__.py @@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en' __all__ = ['dukpy', 'Context', 'undefined', 'JSError', 'to_python'] -import errno, os, sys, numbers, hashlib +import errno, os, sys, numbers, hashlib, json from functools import partial from calibre.constants import plugins @@ -20,6 +20,7 @@ del err Context_, undefined = dukpy.Context, dukpy.undefined fs = ''' +exports.writeFileSync = Duktape.writefile; exports.readFileSync = Duktape.readfile; ''' vm = ''' @@ -43,7 +44,12 @@ exports.runInContext = function(code, ctx) { return handle_result(Duktape.run_in_context(code, ctx)); }; exports.runInThisContext = function(code, options) { - return handle_result(Duktape.run_in_this_context(code, options.filename)); + try { + return eval(code); + } catch (e) { + console.error('Error:' + e + ' while evaluating: ' + options.filename); + throw e; + } }; ''' path = ''' @@ -51,40 +57,79 @@ exports.join = function () { return arguments[0] + '/' + arguments[1]; } ''' util = ''' exports.inspect = function(x) { return x.toString(); }; +exports.inherits = function(ctor, superCtor) { + try { + ctor.super_ = superCtor; + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + } catch(e) { console.log('util.inherits() failed with error:', e); throw e; } +}; +''' + +_assert = ''' +module.exports = function(x) {if (!x) throw x + " is false"; }; +exports.ok = module.exports; +exports.notStrictEqual = exports.strictEqual = exports.deepEqual = function() {}; +''' + +stream = ''' +module.exports = {}; ''' def sha1sum(x): return hashlib.sha1(x).hexdigest() def load_file(base_dirs, builtin_modules, name): - ans = builtin_modules.get(name) - if ans is not None: - return ans - ans = {'fs':fs, 'vm':vm, 'path':path, 'util':util}.get(name) - if ans is not None: - return ans - if not name.endswith('.js'): - name += '.js' - def do_open(*args): - with open(os.path.join(*args), 'rb') as f: - return f.read().decode('utf-8') + try: + ans = builtin_modules.get(name) + if ans is not None: + return [True, ans] + ans = {'fs':fs, 'vm':vm, 'path':path, 'util':util, 'assert':_assert, 'stream':stream}.get(name) + if ans is not None: + return [True, ans] + if not name.endswith('.js'): + name += '.js' + def do_open(*args): + with open(os.path.join(*args), 'rb') as f: + return [True, f.read().decode('utf-8')] - for b in base_dirs: - try: - return do_open(b, name) - except EnvironmentError as e: - if e.errno != errno.ENOENT: - raise - raise EnvironmentError('No module named: %s found in the base directories: %s' % (name, os.pathsep.join(base_dirs))) + for b in base_dirs: + try: + return do_open(b, name) + except EnvironmentError as e: + if e.errno != errno.ENOENT: + raise + raise EnvironmentError('No module named: %s found in the base directories: %s' % (name, os.pathsep.join(base_dirs))) + except Exception as e: + return [False, str(e)] def readfile(path, enc='utf-8'): try: with open(path, 'rb') as f: - return [f.read().decode(enc), None, None] + return [f.read().decode(enc or 'utf-8'), None, None] except UnicodeDecodeError as e: - return None, 0, 'Failed to decode the file: %s with specified encoding: %s' % (path, enc) + return None, '', 'Failed to decode the file: %s with specified encoding: %s' % (path, enc) except EnvironmentError as e: - return [None, errno.errorcode[e.errno], 'Failed to read from file: %s with error: %s' % (path, e.message)] + return [None, errno.errorcode[e.errno], 'Failed to read from file: %s with error: %s' % (path, e.message or e)] + +def writefile(path, data, enc='utf-8'): + if enc == undefined: + enc = 'utf-8' + try: + if isinstance(data, type('')): + data = data.encode(enc or 'utf-8') + with open(path, 'wb') as f: + f.write(data) + except UnicodeEncodeError as e: + return '', 'Failed to encode the data for file: %s with specified encoding: %s' % (path, enc) + except EnvironmentError as e: + return [errno.errorcode[e.errno], 'Failed to write to file: %s with error: %s' % (path, e.message or e)] class Function(object): @@ -127,16 +172,24 @@ def to_python(x): class JSError(Exception): - def __init__(self, e): - e = e.args[0] - if hasattr(e, 'toString()'): - msg = '%s:%s:%s' % (e.fileName, e.lineNumber, e.toString()) - Exception.__init__(self, msg) - self.name = e.name - self.js_message = e.message - self.fileName = e.fileName - self.lineNumber = e.lineNumber - self.stack = e.stack + def __init__(self, ex): + e = ex.args[0] + if isinstance(e, dict): + if 'message' in e: + fn, ln = e.get('fileName'), e.get('lineNumber') + msg = type('')(e['message']) + if ln: + msg = type('')(ln) + ':' + msg + if fn: + msg = type('')(fn) + ':' + msg + Exception.__init__(self, msg) + for k, v in e.iteritems(): + if k != 'message': + setattr(self, k, v) + else: + setattr(self, 'js_message', v) + else: + Exception.__init__(self, type('')(e)) else: # Happens if js code throws a string or integer rather than a # subclass of Error @@ -169,6 +222,10 @@ def run_in_context(code, ctx, options=None): ans = c.eval(code) except JSError as e: return [False, e.as_dict()] + except Exception as e: + import traceback + traceback.print_exc() + return [False, {'message':type('')(e)}] return [True, to_python(ans)] class Context(object): @@ -178,19 +235,24 @@ class Context(object): self.g = self._ctx.g self.g.Duktape.load_file = partial(load_file, base_dirs or (os.getcwdu(),), builtin_modules or {}) self.g.Duktape.pyreadfile = readfile + self.g.Duktape.pywritefile = writefile self.g.Duktape.create_context = partial(create_context, base_dirs) self.g.Duktape.run_in_context = run_in_context - self.g.Duktape.run_in_this_context = self.run_in_this_context self.g.Duktape.cwd = os.getcwdu self.g.Duktape.sha1sum = sha1sum + self.g.Duktape.errprint = lambda *args: print(*args, file=sys.stderr) self.eval(''' console = { log: function() { print(Array.prototype.join.call(arguments, ' ')); }, - error: function() { print(Array.prototype.join.call(arguments, ' ')); }, + error: function() { Duktape.errprint(Array.prototype.join.call(arguments, ' ')); }, debug: function() { print(Array.prototype.join.call(arguments, ' ')); } }; - Duktape.modSearch = function (id, require, exports, module) { return Duktape.load_file(id); } + Duktape.modSearch = function (id, require, exports, module) { + var ans = Duktape.load_file(id); + if (ans[0]) return ans[1]; + throw ans[1]; + } if (!String.prototype.trim) { (function() { @@ -243,32 +305,37 @@ class Context(object): return data; } + Duktape.writefile = function(path, data, encoding) { + var x = Duktape.pywritefile(path, data, encoding); + var errcode = x[0]; var errmsg = x[1]; + if (errmsg !== null) throw {code:errcode, message:errmsg}; + } + process = { 'platform': 'duktape', - 'env': {'HOME': '_HOME_'}, + 'env': {'HOME': _HOME_, 'TERM':_TERM_}, 'exit': function() {}, 'cwd':Duktape.cwd } - ''') + '''.replace( + '_HOME_', json.dumps(os.path.expanduser('~'))).replace('_TERM_', json.dumps(os.environ.get('TERM', ''))), + '') + + def reraise(self, e): + raise JSError(e), None, sys.exc_info()[2] def eval(self, code='', fname='', noreturn=False): try: return self._ctx.eval(code, noreturn, fname) except dukpy.JSError as e: - raise JSError(e) - - def run_in_this_context(self, code, fname=''): - try: - return [True, self._ctx.eval(code, False, fname or '')] - except dukpy.JSError as e: - return [False, JSError(e).as_dict()] + self.reraise(e) def eval_file(self, path, noreturn=False): try: return self._ctx.eval_file(path, noreturn) except dukpy.JSError as e: - raise JSError(e) + self.reraise(e) def test_build(): import unittest diff --git a/src/duktape/context.c b/src/duktape/context.c index b26ad807e8..a0e7788fdd 100644 --- a/src/duktape/context.c +++ b/src/duktape/context.c @@ -108,7 +108,7 @@ static PyObject *DukContext_eval(DukContext *self, PyObject *args, PyObject *kw) temp = duk_to_python(self->ctx, -1); duk_pop(self->ctx); if (temp) { - PyErr_SetObject(JSError, temp); + set_dukpy_error(temp); Py_DECREF(temp); } else PyErr_SetString(PyExc_RuntimeError, "The was an error during eval(), but the error could not be read of the stack"); return NULL; @@ -145,7 +145,7 @@ static PyObject *DukContext_eval_file(DukContext *self, PyObject *args, PyObject temp = duk_to_python(self->ctx, -1); duk_pop(self->ctx); if (temp) { - PyErr_SetObject(JSError, temp); + set_dukpy_error(temp); Py_DECREF(temp); } else PyErr_SetString(PyExc_RuntimeError, "The was an error during eval_file(), but the error could not be read of the stack"); return NULL; diff --git a/src/duktape/conversions.c b/src/duktape/conversions.c index c58bf0df5e..7a7558014f 100644 --- a/src/duktape/conversions.c +++ b/src/duktape/conversions.c @@ -24,7 +24,7 @@ static duk_ret_t python_function_caller(duk_context *ctx) DukContext *dctx; duk_idx_t nargs, i; static char buf1[200], buf2[1024]; - int gil_acquired = 0, ret = 1; + int gil_acquired = 0, ret = 1, err_occured; dctx = DukContext_get(ctx); nargs = duk_get_top(ctx); @@ -60,13 +60,15 @@ static duk_ret_t python_function_caller(duk_context *ctx) Py_DECREF(args); if (!result) { + err_occured = PyErr_Occurred() != NULL; get_repr(func, buf1, 200); - if (!PyErr_Occurred()) { + if (!err_occured) { if (gil_acquired) { dctx->py_thread_state = PyEval_SaveThread(); gil_acquired = 0; } - duk_error(ctx, DUK_ERR_ERROR, "Python function (%s) failed", buf1); + get_repr(func, buf1, 200); + duk_error(ctx, DUK_ERR_ERROR, "Function (%s) failed", buf1); } PyErr_Fetch(&ptype, &pval, &tb); if (!get_repr(pval, buf2, 1024)) get_repr(ptype, buf2, 1024); @@ -76,7 +78,8 @@ static duk_ret_t python_function_caller(duk_context *ctx) dctx->py_thread_state = PyEval_SaveThread(); gil_acquired = 0; } - duk_error(ctx, DUK_ERR_ERROR, "Python function (%s) failed with error: %s", buf1, buf2); + get_repr(func, buf1, 200); + duk_error(ctx, DUK_ERR_ERROR, "Function (%s) failed with error: %s", buf1, buf2); } python_to_duk(ctx, result); @@ -142,7 +145,7 @@ int python_to_duk(duk_context *ctx, PyObject *value) else if (value == Py_False) { duk_push_false(ctx); } - else if (Py_TYPE(value) == &DukObject_Type) { + else if (Py_TYPE(value) == &DukObject_Type || Py_TYPE(value) == &DukFunction_Type || Py_TYPE(value) == &DukArray_Type) { DukObject_push((DukObject *)value, ctx); } else if (PyUnicode_Check(value)) { diff --git a/src/duktape/dukpy.h b/src/duktape/dukpy.h index 2b19cd151f..7952fbca04 100644 --- a/src/duktape/dukpy.h +++ b/src/duktape/dukpy.h @@ -70,5 +70,6 @@ DukEnum *DukEnum_from_DukContext(DukContext *context, dukenum_mode_t mode); int python_to_duk(duk_context *ctx, PyObject *value); PyObject *duk_to_python(duk_context *ctx, duk_idx_t index); +void set_dukpy_error(PyObject *obj); #endif /* DUKPY_H */ diff --git a/src/duktape/errors.c b/src/duktape/errors.c new file mode 100644 index 0000000000..739347db03 --- /dev/null +++ b/src/duktape/errors.c @@ -0,0 +1,45 @@ +/* + * errors.c + * Copyright (C) 2015 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "dukpy.h" + +static int copy_error_attr(PyObject *obj, const char* name, PyObject *dest) { + PyObject *value = NULL; + if (!PyObject_HasAttrString(obj, name)) return NULL; + value = PyObject_GetAttrString(obj, name); + if (value == NULL) return 0; + if (PyDict_SetItemString(dest, name, value) != 0) {Py_DECREF(value); return 0;} + Py_DECREF(value); + return 1; +} + +void set_dukpy_error(PyObject *obj) { + PyObject *err = NULL, *iterator = NULL, *item = NULL; + Py_ssize_t i = 0; + if (Py_TYPE(obj) == &DukObject_Type) { + err = PyDict_New(); + if (err == NULL) { PyErr_NoMemory(); return; } + + // Look for the common error object properties that may be up the prototype chain + if (!copy_error_attr(obj, "name", err)) { Py_DECREF(err); return; } + if (!copy_error_attr(obj, "message", err)) { Py_DECREF(err); return; } + if (!copy_error_attr(obj, "fileName", err)) { Py_DECREF(err); return; } + if (!copy_error_attr(obj, "lineNumber", err)) { Py_DECREF(err); return; } + if (!copy_error_attr(obj, "stack", err)) { Py_DECREF(err); return; } + + // Now copy over own properties + iterator = PyObject_CallMethod(obj, "items", NULL); + if (iterator == NULL) { Py_DECREF(err); return; } + while (item = PyIter_Next(iterator)) { + PyDict_SetItem(err, PyTuple_GET_ITEM(item, 0), PyTuple_GET_ITEM(item, 1)); + Py_DECREF(item); + } + + PyErr_SetObject(JSError, err); + Py_DECREF(err); Py_DECREF(iterator); + } else PyErr_SetObject(JSError, obj); +} diff --git a/src/duktape/proxy.c b/src/duktape/proxy.c index 070cec10cb..36bc63db46 100644 --- a/src/duktape/proxy.c +++ b/src/duktape/proxy.c @@ -1,6 +1,6 @@ #include "dukpy.h" -/* DukObject */ +/* DukObject {{{ */ static void DukObject_INIT(DukObject *self, DukContext *context, duk_idx_t index) @@ -231,9 +231,9 @@ PyTypeObject DukObject_Type = { 0, /* tp_alloc */ 0 /* tp_new */ }; +// }}} - -/* DukArray */ +/* DukArray {{{ */ DukObject *DukArray_from_ctx(duk_context *ctx, duk_idx_t index) { @@ -370,9 +370,9 @@ PyTypeObject DukArray_Type = { 0, /* tp_weaklistoffset */ (getiterfunc)DukArray_iter /* tp_iter */ }; +/// }}} - -/* DukFunction */ +/* DukFunction {{{ */ DukObject *DukFunction_from_ctx(duk_context *ctx, duk_idx_t index) { @@ -392,6 +392,14 @@ DukObject *DukFunction_from_ctx(duk_context *ctx, duk_idx_t index) return self; } +PyObject* DukFunction_repr(DukObject *self) { + PyObject *ans = NULL; + PyObject *name = PyObject_GetAttrString((PyObject*)self, "name"), *fname = PyObject_GetAttrString((PyObject*)self, "fileName"); + ans = PyUnicode_FromFormat("[Function proxy: %S() in filename: %S]", name, fname); + Py_XDECREF(name); Py_XDECREF(fname); + return ans; +} + PyObject* DukFunction_call(DukObject *self, PyObject *args, PyObject *kw) { duk_context *ctx = self->context->ctx; @@ -442,7 +450,7 @@ PyObject* DukFunction_call(DukObject *self, PyObject *args, PyObject *kw) temp = duk_to_python(ctx, -1); duk_pop(ctx); if (temp) { - PyErr_SetObject(JSError, temp); + set_dukpy_error(temp); Py_DECREF(temp); } else PyErr_SetString(PyExc_RuntimeError, "The was an error during call(), but the error could not be read of the stack"); return NULL; @@ -470,7 +478,7 @@ PyTypeObject DukFunction_Type = { 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ - 0, /* tp_repr */ + (reprfunc)DukFunction_repr, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ @@ -483,9 +491,9 @@ PyTypeObject DukFunction_Type = { Py_TPFLAGS_DEFAULT, /* tp_flags */ "Duktape function proxy" /* tp_doc */ }; +// }}} - -/* DukEnum */ +/* DukEnum {{{ */ DukEnum *DukEnum_from_DukContext(DukContext *context, dukenum_mode_t mode) { @@ -575,3 +583,4 @@ PyTypeObject DukEnum_Type = { (getiterfunc)DukEnum_iter, /* tp_iter */ (iternextfunc)DukEnum_iternext, /* tp_iternext */ }; +// }}} diff --git a/src/duktape/tests.py b/src/duktape/tests.py index bbc11e4a90..c981025ab3 100644 --- a/src/duktape/tests.py +++ b/src/duktape/tests.py @@ -30,6 +30,12 @@ class ContextTests(unittest.TestCase): def test_undefined(self): self.assertEqual(repr(undefined), 'undefined') + def test_roundtrip(self): + self.g.g = self.ctx.eval('function f() {return 1;}; f') + self.assertEqual(self.g.g.name, 'f') + self.g.a = self.ctx.eval('[1,2,3]') + self.assertEqual(self.g.a[2], 3) + class ValueTests(unittest.TestCase): @@ -112,8 +118,8 @@ class ValueTests(unittest.TestCase): self.assert_('No error raised for bad function') except JSError as e: e = e.args[0] - self.assertEqual('ReferenceError', e.name) - self.assertIn('nonexistent', e.toString()) + self.assertEqual('ReferenceError', e['name']) + self.assertIn('nonexistent', e['message']) class EvalTests(unittest.TestCase): @@ -148,19 +154,19 @@ class EvalTests(unittest.TestCase): self.assert_('No error raised for malformed js') except JSError as e: e = e.args[0] - self.assertEqual('SyntaxError', e.name) - self.assertEqual('', e.fileName) - self.assertEqual(1, e.lineNumber) - self.assertIn('line 1', e.toString()) + self.assertEqual('SyntaxError', e['name']) + self.assertEqual('', e['fileName']) + self.assertEqual(1, e['lineNumber']) + self.assertIn('line 1', e['message']) try: self.ctx.eval('\na()', fname='xxx') self.assert_('No error raised for malformed js') except JSError as e: e = e.args[0] - self.assertEqual('ReferenceError', e.name) - self.assertEqual('xxx', e.fileName) - self.assertEqual(2, e.lineNumber) + self.assertEqual('ReferenceError', e['name']) + self.assertEqual('xxx', e['fileName']) + self.assertEqual(2, e['lineNumber']) def test_eval_multithreading(self): ev = Event()