diff --git a/src/core/python2js.c b/src/core/python2js.c index 7a12dc5f2..47cabfa51 100644 --- a/src/core/python2js.c +++ b/src/core/python2js.c @@ -86,7 +86,7 @@ int _python2js_remove_from_cache(PyObject* map, PyObject* pyparent); JsRef -_python2js_cache(PyObject* x, PyObject* map); +_python2js_cache(PyObject* x, PyObject* map, int depth); static JsRef _python2js_float(PyObject* x) @@ -148,7 +148,7 @@ _python2js_bytes(PyObject* x) } static JsRef -_python2js_sequence(PyObject* x, PyObject* map) +_python2js_sequence(PyObject* x, PyObject* map, int depth) { JsRef jsarray = hiwire_array(); if (_python2js_add_to_cache(map, x, jsarray)) { @@ -167,7 +167,7 @@ _python2js_sequence(PyObject* x, PyObject* map) Py_INCREF(x); return pyproxy_new(x); } - JsRef jsitem = _python2js_cache(pyitem, map); + JsRef jsitem = _python2js_cache(pyitem, map, depth); if (jsitem == NULL) { _python2js_remove_from_cache(map, x); Py_DECREF(pyitem); @@ -186,7 +186,7 @@ _python2js_sequence(PyObject* x, PyObject* map) } static JsRef -_python2js_dict(PyObject* x, PyObject* map) +_python2js_dict(PyObject* x, PyObject* map, int depth) { JsRef jsdict = hiwire_object(); if (_python2js_add_to_cache(map, x, jsdict)) { @@ -196,13 +196,13 @@ _python2js_dict(PyObject* x, PyObject* map) PyObject *pykey, *pyval; Py_ssize_t pos = 0; while (PyDict_Next(x, &pos, &pykey, &pyval)) { - JsRef jskey = _python2js_cache(pykey, map); + JsRef jskey = _python2js_cache(pykey, map, depth); if (jskey == NULL) { _python2js_remove_from_cache(map, x); hiwire_decref(jsdict); return NULL; } - JsRef jsval = _python2js_cache(pyval, map); + JsRef jsval = _python2js_cache(pyval, map, depth); if (jsval == NULL) { _python2js_remove_from_cache(map, x); hiwire_decref(jskey); @@ -220,8 +220,17 @@ _python2js_dict(PyObject* x, PyObject* map) return jsdict; } +#define RETURN_IF_SUCCEEDS(x) \ + do { \ + JsRef _fresh_result = x; \ + if (_fresh_result != NULL) { \ + return _fresh_result; \ + } \ + PyErr_Clear(); \ + } while (0) + static JsRef -_python2js(PyObject* x, PyObject* map) +_python2js_immutable(PyObject* x, PyObject* map, int depth) { if (x == Py_None) { return hiwire_undefined(); @@ -241,25 +250,38 @@ _python2js(PyObject* x, PyObject* map) return JsProxy_AsJs(x); } else if (JsException_Check(x)) { return JsException_AsJs(x); - } else if (PyList_Check(x) || PyTuple_Check(x)) { - return _python2js_sequence(x, map); - } else if (PyDict_Check(x)) { - return _python2js_dict(x, map); - } else { - JsRef ret = _python2js_buffer(x); + } else if (PyTuple_Check(x)) { + return _python2js_sequence(x, map, depth); + } + return NULL; +} - if (ret != NULL) { - return ret; - } - PyErr_Clear(); - if (PySequence_Check(x)) { - return _python2js_sequence(x, map); - } +static JsRef +_python2js_deep(PyObject* x, PyObject* map, int depth) +{ + RETURN_IF_SUCCEEDS(_python2js_immutable(x, map, depth)); + if (PyList_Check(x)) { + return _python2js_sequence(x, map, depth); + } + if (PyDict_Check(x)) { + return _python2js_dict(x, map, depth); + } + RETURN_IF_SUCCEEDS(_python2js_buffer(x)); - // Proxies we've already created are just returned again, so that the - // same object on the Python side is always the same object on the - // Javascript side. + if (PySequence_Check(x)) { + return _python2js_sequence(x, map, depth); + } + return pyproxy_new(x); +} + +static JsRef +_python2js(PyObject* x, PyObject* map, int depth) +{ + if (depth == 0) { + RETURN_IF_SUCCEEDS(_python2js_immutable(x, map, 0)); return pyproxy_new(x); + } else { + return _python2js_deep(x, map, depth - 1); } } @@ -299,7 +321,7 @@ _python2js_remove_from_cache(PyObject* map, PyObject* pyparent) } JsRef -_python2js_cache(PyObject* x, PyObject* map) +_python2js_cache(PyObject* x, PyObject* map, int depth) { PyObject* id = PyLong_FromSize_t((size_t)x); PyObject* val = PyDict_GetItem(map, id); @@ -310,7 +332,7 @@ _python2js_cache(PyObject* x, PyObject* map) result = hiwire_incref(result); } } else { - result = _python2js(x, map); + result = _python2js(x, map, depth); } Py_DECREF(id); return result; @@ -320,7 +342,7 @@ JsRef python2js(PyObject* x) { PyObject* map = PyDict_New(); - JsRef result = _python2js_cache(x, map); + JsRef result = _python2js_cache(x, map, -1); Py_DECREF(map); if (result == NULL) { @@ -330,10 +352,63 @@ python2js(PyObject* x) return result; } +JsRef +python2js_with_depth(PyObject* x, int depth) +{ + PyObject* map = PyDict_New(); + JsRef result = _python2js_cache(x, map, depth); + Py_DECREF(map); + + if (result == NULL) { + pythonexc2js(); + } + + return result; +} + +PyObject* globals; + +JsRef +test_python2js_with_depth(char* name, int depth) +{ + PyObject* pyname = PyUnicode_FromString(name); + PyObject* pyval = PyDict_GetItem(globals, pyname); + if (pyval == NULL) { + if (!PyErr_Occurred()) { + PyErr_Format(PyExc_KeyError, "%s", name); + } + Py_DECREF(pyname); + pythonexc2js(); + return NULL; + } + + Py_DECREF(pyname); + JsRef idval = python2js_with_depth(pyval, depth); + return idval; +} + int python2js_init() { bool success = false; + PyObject* __main__ = PyImport_AddModule("__main__"); // borrowed! + FAIL_IF_NULL(__main__); + + globals = PyModule_GetDict(__main__); + FAIL_IF_NULL(globals); + + EM_ASM({ + Module.test_python2js_with_depth = function(name, depth) + { + let pyname = stringToNewUTF8(name); + let idresult = _test_python2js_with_depth(pyname, depth); + jsresult = Module.hiwire.get_value(idresult); + Module.hiwire.decref(idresult); + _free(pyname); + return jsresult; + }; + }); + tbmod = PyImport_ImportModule("traceback"); FAIL_IF_NULL(tbmod); success = true; diff --git a/src/tests/test_typeconversions.py b/src/tests/test_typeconversions.py index 0aa22a01c..d475c9749 100644 --- a/src/tests/test_typeconversions.py +++ b/src/tests/test_typeconversions.py @@ -352,3 +352,85 @@ def test_memoryview_conversion(selenium): // (automatically checked in conftest.py) """ ) + + +def test_python2js_with_depth(selenium): + + selenium.run("a = [1, 2, 3]") + assert selenium.run_js( + """ + res = pyodide._module.test_python2js_with_depth("a", -1); + return (Array.isArray(res)) && JSON.stringify(res) === "[1,2,3]"; + """ + ) + + selenium.run("a = (1, 2, 3)") + assert selenium.run_js( + """ + res = pyodide._module.test_python2js_with_depth("a", -1); + return (Array.isArray(res)) && JSON.stringify(res) === "[1,2,3]"; + """ + ) + + selenium.run("a = [(1,2), (3,4), [5, 6], { 2 : 3, 4 : 9}]") + assert selenium.run_js( + """ + res = pyodide._module.test_python2js_with_depth("a", -1); + return Array.isArray(res) && \ + JSON.stringify(res) === `[[1,2],[3,4],[5,6],{"2":3,"4":9}]`; + """ + ) + + selenium.run( + """ + a = [1,[2,[3,[4,[5,[6,[7]]]]]]] + """ + ) + selenium.run_js( + """ + function assert(x, msg){ + if(x !== true){ + throw new Error(`Assertion failed: ${msg}`); + } + } + for(let i=0; i < 7; i++){ + let x = pyodide._module.test_python2js_with_depth("a", i); + for(let j=0; j < i; j++){ + assert(Array.isArray(x), `i: ${i}, j: ${j}`); + x = x[1]; + } + assert(pyodide._module.PyProxy.isPyProxy(x), `i: ${i}, j: ${i}`); + } + """ + ) + + selenium.run("a = [1, (2, (3, [4, (5, (6, [7]))]))]") + selenium.run_js( + """ + function assert(x, msg){ + if(x !== true){ + throw new Error(`Assertion failed: ${msg}`); + } + } + let depths = [0, 3, 3, 3, 6, 6, 6] + for(let i=0; i < 7; i++){ + let x = pyodide._module.test_python2js_with_depth("a", i); + for(let j=0; j < depths[i]; j++){ + assert(Array.isArray(x), `i: ${i}, j: ${j}`); + x = x[1]; + } + assert(pyodide._module.PyProxy.isPyProxy(x), `i: ${i}, j: ${i}`); + } + """ + ) + + +@pytest.mark.xfail +def test_py2js_set(selenium): + selenium.run("a = {1, 2, 3}") + assert selenium.run_js( + """ + let res = pyodide._module.test_python2js_with_depth("a", -1); + return res instanceof Set; + """ + )