Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use JS/Python prototype instead of conditions to choose the correct JS <=> Brython conversion method #2300

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions www/src/brython_builtins.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
283 changes: 153 additions & 130 deletions www/src/js_objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: <JS number>}
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

Expand All @@ -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)
}
}
Expand All @@ -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: <JS number>}
return pyobj.value

}
if(klass === $B.function || klass === $B.method){
function convertMethodsOrFunctions(pyobj, _this) {
if(pyobj.prototype &&
pyobj.prototype.constructor === pyobj &&
! pyobj.$is_func){
Expand All @@ -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)
Expand All @@ -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){
Expand Down
1 change: 1 addition & 0 deletions www/src/py_dict.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading