diff --git a/www/src/brython_builtins.js b/www/src/brython_builtins.js index dc1f0bfe0..c3d4233de 100644 --- a/www/src/brython_builtins.js +++ b/www/src/brython_builtins.js @@ -127,6 +127,34 @@ $B.builtin_funcs = {} // Builtin classes $B.builtin_classes = [] +// enable to list the wrappers + +$B.wrappers_JS2Py = new Map(); +$B.wrappers_Py2JS = new Map(); + +$B.SYMBOL_JS2PY_WRAPPER = Symbol("JS2PY"); +$B.SYMBOL_PY2JS_WRAPPER = Symbol("PY2JS"); + +$B.SYMBOL_JSOBJ = Symbol("JSOBJ"); +$B.SYMBOL_PYOBJ = Symbol("PYOBJ"); + +$B.addJS2PyWrapper = function(jsclass, fct, desc = "") { + if( Object.hasOwnProperty(jsclass.prototype, $B.SYMBOL_JS2PY_WRAPPER) ) { + console.log(jsclass); + throw new Error("A JS2PY Wrapper already has been defined for", jsclass.constructor.name); + } + jsclass.prototype[$B.SYMBOL_JS2PY_WRAPPER] = fct; + fct.desc = desc; + $B.wrappers_JS2Py.set(jsclass, fct); +} +$B.addPy2JSWrapper = function (pyclass, fct, desc = "") { + if( pyclass[$B.SYMBOL_PY2JS_WRAPPER] !== undefined ) + throw new Error("A PY2JS Wrapper already has been defined for", pyclass.__name__); + pyclass[$B.SYMBOL_PY2JS_WRAPPER] = fct; + fct.desc = desc; + $B.wrappers_Py2JS.set(pyclass, fct); +} + $B.__getattr__ = function(attr){return this[attr]} $B.__setattr__ = function(attr, value){ // limited to some attributes diff --git a/www/src/js_objects.js b/www/src/js_objects.js index c6519ed83..4c5a64329 100644 --- a/www/src/js_objects.js +++ b/www/src/js_objects.js @@ -101,57 +101,95 @@ $B.structuredclone2pyobj = function(obj){ } -const JSOBJ = Symbol('JSOBJ') -const PYOBJ = Symbol('PYOBJ') -const PYOBJFCT = Symbol('PYOBJFCT') -const PYOBJFCTS = Symbol('PYOBJFCTS') -var jsobj2pyobj = $B.jsobj2pyobj = function(jsobj, _this){ - // If _this is passed and jsobj is a function, the function is called - // with built-in value `this` set to _this - switch(jsobj) { - case true: - case false: - return jsobj - } +const JSOBJ = $B.SYMBOL_JSOBJ; +const PYOBJ = $B.SYMBOL_PYOBJ; +const JSOBJ_FCT = $B.SYMBOL_PY2JS_WRAPPER; +const PYOBJ_FCT = $B.SYMBOL_JS2PY_WRAPPER; - if(jsobj === undefined){ - return $B.Undefined - } - if(jsobj === null){ - return null - } +const PYOBJFCT = Symbol() +const PYOBJFCTS = Symbol() - if(Array.isArray(jsobj)){ + +$B.addJS2PyWrapper(Array, function(jsobj) { // set it as non-enumerable, prevents issues when looping on it in JS. Object.defineProperty(jsobj, "$is_js_array", {value: true}); + return jsobj // $B.$list(jsobj.map(jsobj2pyobj)) +}, "Array => JSArray"); + +$B.addPy2JSWrapper(_b_.list, function(pyobj) { + + // Python list : transform its elements + return pyobj.map($B.pyobj2jsobj); +}, "list => Array (clone)"); + + +$B.addPy2JSWrapper(_b_.str, function(pyobj) { + // Python strings are converted to the underlying value + return pyobj.valueOf() +}, "string <= str"); + + +$B.addPy2JSWrapper(_b_.dict, function(pyobj) { + + // Python dictionaries are transformed into a Javascript object + // whose attributes are the dictionary keys + // Non-string keys are converted to strings by str(key). This will + // affect Python dicts such as {"1": 'a', 1: "b"}, the result will + // be the Javascript object {1: "b"} + let jsobj = {} + for(var entry of _b_.dict.$iter_items_with_hash(pyobj)){ + var key = entry.key + if(typeof key !== "string"){ + key = _b_.str.$factory(key) + } + if(typeof entry.value === 'function'){ + // set "this" to jsobj + entry.value.bind(jsobj) + } + jsobj[key] = $B.pyobj2jsobj(entry.value) + } return jsobj - } - if(typeof jsobj === 'number'){ - if(jsobj % 1 === 0){ //TODO: dangerous, it can also be a float with no decimals. +}, "JSObj <= dict"); + +$B.addPy2JSWrapper(_b_.tuple, function(pyobj) { + + // Python list : transform its elements + return pyobj.map($B.pyobj2jsobj); +}, "tuple => Array (clone)"); + +//TODO: optimize unwrap... +$B.addJS2PyWrapper(Boolean, function(jsobj){ + return jsobj; +}, 'Boolean => Boolean'); +$B.addJS2PyWrapper(Number, function(jsobj){ + if(jsobj % 1 === 0){ //TODO: DANGEROUS! It can also be a float with no decimals. return _b_.int.$factory(jsobj) } // for now, lets assume a float return _b_.float.$factory(jsobj) - } - if(typeof jsobj == "string"){ - return $B.String(jsobj) - } - if(typeof jsobj == 'bigint'){ - return _b_.int.$int_or_long(jsobj) - } - let pyobj = jsobj[PYOBJ] - if(pyobj !== undefined) { - return pyobj; - } +}, 'Number => PyInt or PyFloat'); + - if(jsobj instanceof Promise){ +$B.addPy2JSWrapper(_b_.float, function(pyobj) { + + // floats are implemented as + // {__class__: _b_.float, value: } + return pyobj.value // dangerous => can be later converted as int when browser fetch it back. +}, 'Number <= PyFloat'); + +$B.addJS2PyWrapper(String, function(jsobj){ + return $B.String(jsobj); +}, "String => PyString"); + +$B.addJS2PyWrapper(Promise, function(jsobj) { // cf. issue #2321 return jsobj.then(x => jsobj2pyobj(x)).catch($B.handle_error) - } - - if(typeof jsobj === "function"){ +}, "Promise => Promise Wrapper"); + +$B.addJS2PyWrapper(Function, function(jsobj, _this) { + // transform Python arguments to equivalent JS arguments _this = _this === undefined ? null : _this @@ -178,8 +216,10 @@ var jsobj2pyobj = $B.jsobj2pyobj = function(jsobj, _this){ args[i] = pyobj2jsobj(arguments[i]) } try{ - return jsobj2pyobj(jsobj.apply(_this, args)) + const ret = jsobj.apply(_this, args); + return jsobj2pyobj(ret); }catch(err){ + console.log(err); throw $B.exception(err) } } @@ -198,98 +238,10 @@ var jsobj2pyobj = $B.jsobj2pyobj = function(jsobj, _this){ __qualname__: jsobj.name } return res - } - - if(jsobj.$kw){ - return jsobj - } - - if($B.$isNode(jsobj)){ - const res = $B.DOMNode.$factory(jsobj) - jsobj[PYOBJ] = res - res[JSOBJ] = jsobj - return res - } - - const _res = $B.JSObj.$factory(jsobj) - jsobj[PYOBJ] = _res - _res[JSOBJ] = jsobj - - return _res; -} - -var pyobj2jsobj = $B.pyobj2jsobj = function(pyobj){ - // conversion of a Python object into a Javascript object - if(pyobj === true || pyobj === false){ - return pyobj - } - if(pyobj === $B.Undefined){ - return undefined - } - if(pyobj === null) { - return null - } +}, "Function => Wrapper"); - let _jsobj = pyobj[JSOBJ] - if(_jsobj !== undefined){ - return _jsobj - } - var klass = $B.get_class(pyobj) - if(klass === undefined){ - // not a Python object, consider arg as Javascript object instead - return pyobj - } - - if(klass === $B.DOMNode || - klass.__mro__.indexOf($B.DOMNode) > -1){ - - // instances of DOMNode or its subclasses are transformed into the - // underlying DOM element - return pyobj - - } - if([_b_.list, _b_.tuple].indexOf(klass) > -1){ - - // Python list : transform its elements - return pyobj.map(pyobj2jsobj) - - } - if(klass === _b_.dict || _b_.issubclass(klass, _b_.dict)){ - - // Python dictionaries are transformed into a Javascript object - // whose attributes are the dictionary keys - // Non-string keys are converted to strings by str(key). This will - // affect Python dicts such as {"1": 'a', 1: "b"}, the result will - // be the Javascript object {1: "b"} - var jsobj = {} - for(var entry of _b_.dict.$iter_items_with_hash(pyobj)){ - var key = entry.key - if(typeof key !== "string"){ - key = _b_.str.$factory(key) - } - if(typeof entry.value === 'function'){ - // set "this" to jsobj - entry.value.bind(jsobj) - } - jsobj[key] = pyobj2jsobj(entry.value) - } - return jsobj - - } - if(klass === _b_.str){ - // Python strings are converted to the underlying value - return pyobj.valueOf() - - } - if(klass === _b_.float){ - - // floats are implemented as - // {__class__: _b_.float, value: } - return pyobj.value - - } - if(klass === $B.function || klass === $B.method){ +function convertMethodsOrFunctions(pyobj, _this) { if(pyobj.prototype && pyobj.prototype.constructor === pyobj && ! pyobj.$is_func){ @@ -311,7 +263,7 @@ var pyobj2jsobj = $B.pyobj2jsobj = function(pyobj){ return jsobj } // Transform into a Javascript function - var jsobj = function(){ + const jsobj = function(){ try{ // transform JS arguments to Python arguments var args = new Array(arguments.length) @@ -334,9 +286,80 @@ var pyobj2jsobj = $B.pyobj2jsobj = function(pyobj){ pyobj[JSOBJ] = jsobj jsobj[PYOBJ] = pyobj + return jsobj +} + +$B.addPy2JSWrapper($B.function, convertMethodsOrFunctions); +$B.addPy2JSWrapper($B.method , convertMethodsOrFunctions, "? <= method or function"); + +$B.addJS2PyWrapper(Object, function(jsobj, _this) { //TODO: exclude isNode... + + if(jsobj.$kw){ // we really shouldn't be doing that... return jsobj } - return pyobj + + if( typeof jsobj === "bigint") { // bigint is in fact converted to an object... + return _b_.int.$int_or_long(jsobj); + } + + const _res = $B.JSObj.$factory(jsobj) + jsobj[PYOBJ] = _res + _res[JSOBJ] = jsobj + + return _res; +}, "Object => PyObj + BigInt => Number"); + +var jsobj2pyobj = $B.jsobj2pyobj = function(jsobj, _this){ + // If _this is passed and jsobj is a function, the function is called + // with built-in value `this` set to _this + + // handle undefined and null first => cause issues... + if(jsobj === undefined) + return $B.Undefined; + if(jsobj === null) + return null; + + const pyobj = jsobj[PYOBJ]; + if(pyobj !== undefined) + return pyobj; + + const fct = jsobj[PYOBJ_FCT]; + + return fct(jsobj, _this); +} + +var pyobj2jsobj = $B.pyobj2jsobj = function(pyobj){ + // conversion of a Python object into a Javascript object + + // handle undefined and null first => cause issues... + if(pyobj === $B.Undefined) + return undefined + + if( ! (pyobj instanceof Object) ) // not a python type (not even an object)... + return pyobj; + + + const klass = $B.get_class(pyobj) + if(klass === undefined){ + // not a Python object, consider arg as Javascript object instead + return pyobj + } + + let jsobj = pyobj[JSOBJ] + if(jsobj !== undefined) + return jsobj + + const mro = klass.__mro__; + let jsobj_fct = klass[JSOBJ_FCT]; + let offset = 0; + while ( jsobj_fct === undefined && offset < mro.length ) { + jsobj_fct = mro[offset++][JSOBJ_FCT]; + } + + if(jsobj_fct !== undefined) + return jsobj_fct(pyobj); + + return pyobj // no convertion known... } function pyargs2jsargs(pyargs){ diff --git a/www/src/py_dict.js b/www/src/py_dict.js index 7a46bf956..c4d8577e2 100644 --- a/www/src/py_dict.js +++ b/www/src/py_dict.js @@ -154,6 +154,7 @@ var dict = { $match_mapping_pattern: true // for pattern matching (PEP 634) } + dict.$to_obj = function(d){ // Function applied to dictionary that only has string keys, // return a Javascript objects with the keys mapped to the value, diff --git a/www/src/py_dom.js b/www/src/py_dom.js index f6a328b95..a969234ca 100644 --- a/www/src/py_dom.js +++ b/www/src/py_dom.js @@ -468,6 +468,22 @@ var DOMNode = $B.make_class('DOMNode', } ) + +$B.addJS2PyWrapper(Node, function(jsobj, _this) { + + const res = $B.DOMNode.$factory(jsobj) + jsobj[PYOBJ] = res + res[JSOBJ] = jsobj + return res +}); + +$B.addPy2JSWrapper(DOMNode, function(pyobj) { + + // instances of DOMNode or its subclasses are transformed into the + // underlying DOM element + return pyobj +}); + DOMNode.__add__ = function(self, other){ // adding another element to self returns an instance of TagSum var res = TagSum.$factory() diff --git a/www/src/py_exceptions.js b/www/src/py_exceptions.js index c613ea764..0ef67d759 100644 --- a/www/src/py_exceptions.js +++ b/www/src/py_exceptions.js @@ -1145,6 +1145,9 @@ $B.show_error = function(err){ } $B.handle_error = function(err){ + + console.log(err); + // Print the error traceback on the standard error stream if(err.$handled){ return diff --git a/www/src/py_list.js b/www/src/py_list.js index 2516d6492..ef4a092bc 100644 --- a/www/src/py_list.js +++ b/www/src/py_list.js @@ -29,6 +29,7 @@ var list = { __dir__: object.__dir__ } + list.__add__ = function(self, other){ if($B.get_class(self) !== $B.get_class(other)){ var this_name = $B.class_name(self) // can be tuple @@ -1036,4 +1037,5 @@ _b_.tuple = tuple _b_.object.__bases__ = tuple.$factory() _b_.type.__bases__ = $B.fast_tuple([_b_.object]) + })(__BRYTHON__)