diff --git a/CHANGELOG.md b/CHANGELOG.md index eb940a1d1..ce2658934 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ **User improvements:** +- Thanks to PEP 562, you can now `import js` from Python and use it to access + anything in the global Javascript namespace. + - Passing a Python object to Javascript always creates the same object in Javascript. This makes APIs like `removeEventListener` usable. diff --git a/docs/type_conversions.md b/docs/type_conversions.md index a8a997662..bc30d8d43 100644 --- a/docs/type_conversions.md +++ b/docs/type_conversions.md @@ -154,10 +154,42 @@ var sys = pyodide.pyimport('sys'); ## Using Javascript objects from Python -Javascript objects can be accessed from Python using the `from js import ...` -syntax. The object must be in the global (`window`) namespace. +Javascript objects can be accessed from Python using the special `js` module. +This module looks up attributes of the global (`window`) namespace on the +Javascript side. ```python -from js import document -document.title = 'New window title' +import js +js.document.title = 'New window title' +``` + +### Performance considerations + +Looking up and converting attributes of the `js` module happens dynamically. In +most cases, where the value is small or results in a proxy, this is not an +issue. However, if the value takes a long time to convert from Javascript to +Python, you may want to store it in a Python variable or use the `from js import +...` syntax. + +For example, given this large Javascript variable: + +```javascript +var x = new Array(1000).fill(0) +``` + +Use it from Python as follows: + +```python +import js +x = js.x # conversion happens once here +for i in range(len(x)): + item = x[i] # we don't pay the conversion price each time here +``` + +Or alternatively: + +```python +from js import x # conversion happens once here +for i in range(len(x)): + item = x[i] # we don't pay the conversion price each time here ``` diff --git a/packages/matplotlib/src/wasm_backend.py b/packages/matplotlib/src/wasm_backend.py index a16ab91c0..f60ffe774 100644 --- a/packages/matplotlib/src/wasm_backend.py +++ b/packages/matplotlib/src/wasm_backend.py @@ -103,10 +103,10 @@ class FigureCanvasWasm(backend_agg.FigureCanvasAgg): def create_root_element(self): # Designed to be overridden by subclasses for use in contexts other # than iodide. - from js import iodide - if iodide is not None: + try: + from js import iodide return iodide.output.element('div') - else: + except ImportError: return document.createElement('div') def show(self): diff --git a/src/hiwire.c b/src/hiwire.c index 73ad7be2d..3922f4e05 100644 --- a/src/hiwire.c +++ b/src/hiwire.c @@ -183,7 +183,11 @@ EM_JS(void, hiwire_push_object_pair, (int idobj, int idkey, int idval), { EM_JS(int, hiwire_get_global, (int idname), { var jsname = UTF8ToString(idname); - return Module.hiwire_new_value(self[jsname]); + if (jsname in self) { + return Module.hiwire_new_value(self[jsname]); + } else { + return -1; + } }); EM_JS(int, hiwire_get_member_string, (int idobj, int idkey), { diff --git a/src/jsimport.c b/src/jsimport.c index a55201b4f..60dfd0cca 100644 --- a/src/jsimport.c +++ b/src/jsimport.c @@ -5,128 +5,71 @@ #include "hiwire.h" #include "js2python.h" -static PyObject* original__import__; -PyObject* globals = NULL; -PyObject* original_globals = NULL; - -typedef struct -{ - PyObject_HEAD -} JsImport; +static PyObject* js_module = NULL; static PyObject* -JsImport_Call(PyObject* self, PyObject* args, PyObject* kwargs) +JsImport_GetAttr(PyObject* self, PyObject* attr) { - PyObject* name = PyTuple_GET_ITEM(args, 0); - if (PyUnicode_CompareWithASCIIString(name, "js") == 0) { - PyObject* locals = PyTuple_GET_ITEM(args, 2); - PyObject* fromlist = PyTuple_GET_ITEM(args, 3); - Py_ssize_t n = PySequence_Size(fromlist); - PyObject* jsmod = PyModule_New("js"); - PyObject* d = PyModule_GetDict(jsmod); - - int is_star = 0; - if (n == 1) { - PyObject* firstfromlist = PySequence_GetItem(fromlist, 0); - if (PyUnicode_CompareWithASCIIString(firstfromlist, "*") == 0) { - is_star = 1; - } - Py_DECREF(firstfromlist); - } - - if (is_star) { - PyErr_SetString(PyExc_ImportError, "'import *' not supported for js"); - return NULL; - } else { - for (Py_ssize_t i = 0; i < n; ++i) { - PyObject* key = PySequence_GetItem(fromlist, i); - if (key == NULL) { - return NULL; - } - const char* c = PyUnicode_AsUTF8(key); - if (c == NULL) { - Py_DECREF(key); - return NULL; - } - int jsval = hiwire_get_global((int)c); - PyObject* pyval = js2python(jsval); - hiwire_decref(jsval); - if (PyDict_SetItem(d, key, pyval)) { - Py_DECREF(key); - Py_DECREF(pyval); - return NULL; - } - Py_DECREF(key); - Py_DECREF(pyval); - } - } - - return jsmod; - } else { - // Fallback to the standard Python import - return PyObject_Call(original__import__, args, kwargs); + const char* c = PyUnicode_AsUTF8(attr); + if (c == NULL) { + return NULL; } + int idval = hiwire_get_global((int)c); + if (idval == -1) { + PyErr_Format(PyExc_AttributeError, "Unknown attribute '%s'", attr); + return NULL; + } + PyObject* result = js2python(idval); + hiwire_decref(idval); + return result; } -static PyTypeObject JsImportType = { - .tp_name = "JsImport", - .tp_basicsize = sizeof(JsImport), - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_call = JsImport_Call, - .tp_doc = "An import hook that imports things from Javascript." +static PyObject* +JsImport_Dir() +{ + int idwindow = hiwire_get_global((int)"self"); + int iddir = hiwire_dir(idwindow); + hiwire_decref(idwindow); + PyObject* pydir = js2python(iddir); + hiwire_decref(iddir); + return pydir; +} + +static PyMethodDef JsModule_Methods[] = { + { "__getattr__", + (PyCFunction)JsImport_GetAttr, + METH_O, + "Get an object from the global Javascript namespace" }, + { "__dir__", + (PyCFunction)JsImport_Dir, + METH_NOARGS, + "Returns a list of object name in the global Javascript namespace" }, + { NULL } }; -static PyObject* -JsImport_New() -{ - JsImport* self; - self = (JsImport*)JsImportType.tp_alloc(&JsImportType, 0); - return (PyObject*)self; -} +static struct PyModuleDef JsModule = { + PyModuleDef_HEAD_INIT, + "js", + "Provides access to Javascript global variables from Python", + 0, + JsModule_Methods +}; int JsImport_init() { - if (PyType_Ready(&JsImportType)) { + PyObject* module_dict = PyImport_GetModuleDict(); + if (module_dict == NULL) { return 1; } - PyObject* m = PyImport_AddModule("builtins"); - if (m == NULL) { + js_module = PyModule_Create(&JsModule); + if (js_module == NULL) { return 1; } - PyObject* d = PyModule_GetDict(m); - if (d == NULL) { - return 1; - } - - original__import__ = PyDict_GetItemString(d, "__import__"); - if (original__import__ == NULL) { - return 1; - } - Py_INCREF(original__import__); - - PyObject* importer = JsImport_New(); - if (importer == NULL) { - return 1; - } - - if (PyDict_SetItemString(d, "__import__", importer)) { - return 1; - } - - m = PyImport_AddModule("__main__"); - if (m == NULL) { - return 1; - } - - globals = PyModule_GetDict(m); - if (globals == NULL) { - return 1; - } - - if (PyDict_Update(globals, d)) { + if (PyDict_SetItemString(module_dict, "js", js_module)) { + Py_DECREF(js_module); return 1; } diff --git a/src/jsimport.h b/src/jsimport.h index 04be34127..4b086668c 100644 --- a/src/jsimport.h +++ b/src/jsimport.h @@ -9,6 +9,4 @@ int JsImport_init(); -extern PyObject* globals; - #endif /* JSIMPORT_H */ diff --git a/src/runpython.c b/src/runpython.c index 605a60cc0..0fddcafb9 100644 --- a/src/runpython.c +++ b/src/runpython.c @@ -7,7 +7,7 @@ #include "hiwire.h" #include "python2js.h" -extern PyObject* globals; +PyObject* globals; PyObject* eval_code; PyObject* find_imports; @@ -111,6 +111,30 @@ EM_JS(int, runpython_init_js, (), { int runpython_init_py() { + PyObject* builtins = PyImport_AddModule("builtins"); + if (builtins == NULL) { + return 1; + } + + PyObject* builtins_dict = PyModule_GetDict(builtins); + if (builtins_dict == NULL) { + return 1; + } + + PyObject* __main__ = PyImport_AddModule("__main__"); + if (__main__ == NULL) { + return 1; + } + + globals = PyModule_GetDict(__main__); + if (globals == NULL) { + return 1; + } + + if (PyDict_Update(globals, builtins_dict)) { + return 1; + } + PyObject* m = PyImport_ImportModule("pyodide"); if (m == NULL) { return 1; @@ -131,8 +155,6 @@ runpython_init_py() return 1; } - Py_DECREF(m); - Py_DECREF(d); return 0; } diff --git a/test/test_python.py b/test/test_python.py index a0df59a4c..152f13b40 100644 --- a/test/test_python.py +++ b/test/test_python.py @@ -303,11 +303,18 @@ def test_typed_arrays(selenium, wasm_heap, jstype, pytype): def test_import_js(selenium): result = selenium.run( """ - from js import window - window.title = 'Foo' - window.title + import js + js.window.title = 'Foo' + js.window.title """) assert result == 'Foo' + result = selenium.run( + """ + dir(js) + """) + assert len(result) > 100 + assert 'document' in result + assert 'window' in result def test_pyimport_multiple(selenium):