Fix #401: Better error message when using "import js" (#404)

* Fix #401: Use PEP 562 to make "import js" work

* Add docs

* Fix handling of missing attributes

* Add CHANGELOG

* Fix iodide import
This commit is contained in:
Michael Droettboom 2019-05-01 14:56:30 -04:00 committed by GitHub
parent 6e561d2b19
commit c8db5b6433
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 129 additions and 120 deletions

View File

@ -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.

View File

@ -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
```

View File

@ -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):

View File

@ -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), {

View File

@ -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;
}

View File

@ -9,6 +9,4 @@
int
JsImport_init();
extern PyObject* globals;
#endif /* JSIMPORT_H */

View File

@ -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;
}

View File

@ -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):