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

Make a pyproxy of an awaitable python object an awaitable javascript object #1170

Merged
merged 53 commits into from
Feb 14, 2021
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
25deca5
Add simple web loop
Jan 20, 2021
a1c30a9
import asyncio for all webloop tests
Jan 21, 2021
f5bd0b8
Initial implementation, no testing yet
Jan 23, 2021
ec1308e
Merge branch 'simple-loop' into await-pyproxy
Jan 23, 2021
890cd87
Add tests
Jan 23, 2021
ba4dd5e
Added double await coroutine test, lint
Jan 23, 2021
5ed3b1e
Merge branch 'master' into simple-loop
Jan 31, 2021
f2d68ae
Merge branch 'simple-loop' into await-pyproxy
Jan 31, 2021
9cb9e6a
Update changelog
Jan 31, 2021
703bea8
Retest
Feb 1, 2021
2636d4a
Retest
Feb 1, 2021
a0c96ee
Retest
Feb 3, 2021
8eaef2f
Update src/pyodide-py/pyodide/__init__.py
hoodmane Feb 3, 2021
84e5df5
Update src/pyodide-py/pyodide/simple_web_loop.py
hoodmane Feb 3, 2021
0e59553
Update src/pyodide-py/pyodide/simple_web_loop.py
hoodmane Feb 3, 2021
94bc473
Update src/pyodide-py/pyodide/simple_web_loop.py
hoodmane Feb 3, 2021
ebdfba6
Updated doc comments for webloop, renamed webloop
Feb 4, 2021
f56d008
Fix name
Feb 4, 2021
8a4d5a2
Merge branch 'master' into simple-loop
Feb 4, 2021
3f81d65
Add comment about task._source_traceback
Feb 4, 2021
d55cd4d
Update docs
Feb 4, 2021
c26fb70
Update changelog
Feb 4, 2021
ec3abad
Merge branch 'master' into simple-loop
Feb 4, 2021
20da79d
Fix merge conflict
Feb 4, 2021
23084de
Use "create_task" instead of "ensure_future"
Feb 4, 2021
61eabc9
More ensure_future ==> create_task
Feb 4, 2021
ed503f8
One create_task back to ensure_future
Feb 4, 2021
291ad07
Start event loop automatically on init
Feb 4, 2021
5f9b450
Simplify WebLoopPolicy
Feb 4, 2021
6833db1
Lint
Feb 4, 2021
90655bc
Revert ensure_future ==> create_task changes
Feb 5, 2021
55a277b
Merge branch 'simple-loop' into await-pyproxy
Feb 5, 2021
7754eaa
lint
Feb 5, 2021
0fe4299
Fix changelog link
Feb 5, 2021
141a39f
Retest
Feb 5, 2021
e6516e5
Merge branch 'master' into await-pyproxy
Feb 5, 2021
50a3e7b
Merge branch 'master' into await-pyproxy
Feb 6, 2021
053417b
Merge branch 'master' into await-pyproxy
Feb 6, 2021
8629979
Merge branch 'master' into await-pyproxy
Feb 9, 2021
d1d3f3b
Add missing argument list terminator
Feb 9, 2021
bc965a0
Merge branch 'master' into await-pyproxy
Feb 10, 2021
814b08b
Added a few more tests
Feb 10, 2021
8836b7a
Updated docstrings for async evaluation methods, export eval_async
Feb 10, 2021
c2d80a2
Added doc comments to pyproxy.c
Feb 10, 2021
99d1e8c
lint
Feb 10, 2021
50697ee
more lint
Feb 10, 2021
d497c3d
Export eval_code_async
Feb 10, 2021
a509e15
Fix test_pyproxy_mixins
Feb 10, 2021
1be463a
Try to fix test again
Feb 10, 2021
75f8a89
Still trying to get the test right
Feb 10, 2021
6cfa737
Add missing clang-format on
rth Feb 14, 2021
6f5c981
Re-apply clang-format
rth Feb 14, 2021
8f7afd1
Merge remote-tracking branch 'upstream/master' into await-pyproxy
rth Feb 14, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 12 additions & 8 deletions docs/project/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,18 @@
raise a `KeyboardInterrupt` by writing to the interrupt buffer.
[#1148](https://github.com/iodide-project/pyodide/pull/1148) and
[#1173](https://github.com/iodide-project/pyodide/pull/1173)
- A `JsProxy` of a Javascript `Promise` or other awaitable object is now a
Python awaitable.
[#880](https://github.com/iodide-project/pyodide/pull/880)
- Added a Python event loop to support asyncio by scheduling coroutines to run
as jobs on the browser event loop. This event loop is available by default and
automatically enabled by any relevant asyncio API, so for instance
`asyncio.ensure_future` works without any configuration.
[#1158](https://github.com/iodide-project/pyodide/pull/1158)
- A `PyProxy` of a Python coroutine or awaitable is now an awaitable javascript
object. Awaiting a coroutine will schedule it to run on the Python event loop
using `asyncio.ensure_future`.
[#1170](https://github.com/iodide-project/pyodide/pull/1170)
- A `JsProxy` of a Javascript `Promise` or other awaitable object is now a
Python awaitable.
[#880](https://github.com/iodide-project/pyodide/pull/880)
- Made PyProxy of an iterable Python object an iterable Js object: defined the
`[Symbol.iterator]` method, can be used like `for(let x of proxy)`.
Made a PyProxy of a Python iterator an iterator: `proxy.next()` is
Expand All @@ -88,10 +92,6 @@
- JsBoundMethod is now a subclass of JsProxy, which fixes nested attribute
access and various other strange bugs.
[#1124](https://github.com/iodide-project/pyodide/pull/1124)
- In console.html: sync behavior, full stdout/stderr support, clean namespace,
bigger font, correct result representation, clean traceback
[#1125](https://github.com/iodide-project/pyodide/pull/1125) and
[#1141](https://github.com/iodide-project/pyodide/pull/1141)
- Javascript functions imported like `from js import fetch` no longer trigger
"invalid invocation" errors (issue
[#461](https://github.com/iodide-project/pyodide/issues/461)) and
Expand All @@ -100,11 +100,15 @@
[#1126](https://github.com/iodide-project/pyodide/pull/1126)
- Javascript bound method calls now work correctly with keyword arguments.
[#1138](https://github.com/iodide-project/pyodide/pull/1138)
- In console.html: sync behavior, full stdout/stderr support, clean namespace,
bigger font, correct result representation, clean traceback
[#1125](https://github.com/iodide-project/pyodide/pull/1125) and
[#1141](https://github.com/iodide-project/pyodide/pull/1141)
- Switched from ̀Jedi to rlcompleter for completion in
`pyodide.console.InteractiveConsole` and so in `console.html`. This fixes
some completion issues (see
[#821](https://github.com/iodide-project/pyodide/issues/821) and
[#1160](https://github.com/iodide-project/pyodide/issues/821)
[#1160](https://github.com/iodide-project/pyodide/issues/1160)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should re-organize it by submodule as we did for the last release as now it's getting a bit difficult to find things. No action needed in this PR.


## Version 0.16.1
*December 25, 2020*
Expand Down
198 changes: 197 additions & 1 deletion src/core/pyproxy.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
#include "js2python.h"
#include "python2js.h"

_Py_IDENTIFIER(result);
_Py_IDENTIFIER(ensure_future);
_Py_IDENTIFIER(add_done_callback);

static PyObject* asyncio;

JsRef
_pyproxy_repr(PyObject* pyobj)
{
Expand Down Expand Up @@ -223,6 +229,137 @@ _pyproxy_destroy(PyObject* ptrobj)
EM_ASM({ delete Module.PyProxies[$0]; }, ptrobj);
}

bool
_pyproxy_is_awaitable(PyObject* pyobject)
{
PyObject* awaitable = _PyCoro_GetAwaitableIter(pyobject);
PyErr_Clear();
bool result = awaitable != NULL;
Py_CLEAR(awaitable);
return result;
}

// clang-format off
typedef struct {
PyObject_HEAD
JsRef resolve_handle;
JsRef reject_handle;
} FutureDoneCallback;
// clang-format on

static void
FutureDoneCallback_dealloc(FutureDoneCallback* self)
{
hiwire_CLEAR(self->resolve_handle);
hiwire_CLEAR(self->reject_handle);
Py_TYPE(self)->tp_free((PyObject*)self);
}

int
FutureDoneCallback_call_resolve(FutureDoneCallback* self, PyObject* result)
{
hoodmane marked this conversation as resolved.
Show resolved Hide resolved
bool success = false;
JsRef result_js = NULL;
JsRef idargs = NULL;
JsRef output = NULL;
result_js = python2js(result);
idargs = hiwire_array();
hiwire_push_array(idargs, result_js);
output = hiwire_call(self->resolve_handle, idargs);

success = true;
finally:
hiwire_CLEAR(result_js);
hiwire_CLEAR(idargs);
hiwire_CLEAR(output);
return success ? 0 : -1;
}

int
FutureDoneCallback_call_reject(FutureDoneCallback* self)
{
bool success = false;
JsRef excval = NULL;
JsRef idargs = NULL;
JsRef result = NULL;
excval = wrap_exception();
FAIL_IF_NULL(excval);
idargs = hiwire_array();
hiwire_push_array(idargs, excval);
result = hiwire_call(self->reject_handle, idargs);

success = true;
finally:
hiwire_CLEAR(excval);
hiwire_CLEAR(idargs);
hiwire_CLEAR(result);
return success ? 0 : -1;
}

PyObject*
FutureDoneCallback_call(FutureDoneCallback* self,
PyObject* args,
PyObject* kwargs)
{
PyObject* fut;
if (!PyArg_UnpackTuple(args, "future_done_callback", 1, 1, &fut)) {
return NULL;
}
PyObject* result = _PyObject_CallMethodIdObjArgs(fut, &PyId_result, NULL);
int errcode;
if (result != NULL) {
errcode = FutureDoneCallback_call_resolve(self, result);
} else {
errcode = FutureDoneCallback_call_reject(self);
}
if (errcode == 0) {
Py_RETURN_NONE;
} else {
return NULL;
}
}

// clang-format off
static PyTypeObject FutureDoneCallbackType = {
.tp_name = "FutureDoneCallback",
.tp_doc = "Callback for internal use to allow awaiting a future from javascript",
.tp_basicsize = sizeof(FutureDoneCallback),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_dealloc = (destructor) FutureDoneCallback_dealloc,
.tp_call = (ternaryfunc) FutureDoneCallback_call,
};

rth marked this conversation as resolved.
Show resolved Hide resolved
static PyObject *
FutureDoneCallback_cnew(JsRef resolve_handle, JsRef reject_handle)
{
FutureDoneCallback *self = (FutureDoneCallback *) FutureDoneCallbackType.tp_alloc(&FutureDoneCallbackType, 0);
self->resolve_handle = hiwire_incref(resolve_handle);
self->reject_handle = hiwire_incref(reject_handle);
return (PyObject *) self;
}


int
_pyproxy_ensure_future(PyObject* pyobject, JsRef resolve_handle, JsRef reject_handle){
bool success = false;
PyObject* future = NULL;
PyObject* callback = NULL;
PyObject* retval = NULL;
future = _PyObject_CallMethodIdObjArgs(asyncio, &PyId_ensure_future, pyobject, NULL);
FAIL_IF_NULL(future);
callback = FutureDoneCallback_cnew(resolve_handle, reject_handle);
retval = _PyObject_CallMethodIdObjArgs(future, &PyId_add_done_callback, callback, NULL);
FAIL_IF_NULL(retval);

success = true;
finally:
Py_CLEAR(future);
Py_CLEAR(callback);
Py_CLEAR(retval);
return success ? 0 : -1;
}

EM_JS_REF(JsRef, pyproxy_new, (PyObject * ptrobj), {
// Technically, this leaks memory, since we're holding on to a reference
// to the proxy forever. But we have that problem anyway since we don't
Expand All @@ -249,11 +386,15 @@ EM_JS_REF(JsRef, pyproxy_new, (PyObject * ptrobj), {
}
// clang-format on
Module.PyProxies[ptrobj] = proxy;
let is_awaitable = __pyproxy_is_awaitable(ptrobj);
if (is_awaitable) {
Object.assign(target, Module.PyProxyAwaitableMethods);
}

return Module.hiwire.new_value(proxy);
});

EM_JS(int, pyproxy_init, (), {
EM_JS_NUM(int, pyproxy_init_js, (), {
// clang-format off
Module.PyProxies = {};
function _getPtr(jsobj) {
Expand Down Expand Up @@ -479,6 +620,61 @@ EM_JS(int, pyproxy_init, (), {
},
};

Module.PyProxyAwaitableMethods = {
_ensure_future : function(){
let resolve_handle_id = 0;
let reject_handle_id = 0;
let resolveHandle;
let rejectHandle;
let promise;
try {
promise = new Promise((resolve, reject) => {
resolveHandle = resolve;
rejectHandle = reject;
});
resolve_handle_id = Module.hiwire.new_value(resolveHandle);
reject_handle_id = Module.hiwire.new_value(rejectHandle);
let ptrobj = _getPtr(this);
let errcode = __pyproxy_ensure_future(ptrobj, resolve_handle_id, reject_handle_id);
if(errcode === -1){
_pythonexc2js();
}
} finally {
Module.hiwire.decref(resolve_handle_id);
Module.hiwire.decref(reject_handle_id);
}
return promise;
},
then : function(onFulfilled, onRejected){
let promise = this._ensure_future();
return promise.then(onFulfilled, onRejected);
},
catch : function(onRejected){
let promise = this._ensure_future();
return promise.catch(onRejected);
},
finally : function(onFinally){
let promise = this._ensure_future();
return promise.finally(onFinally);
}
};

return 0;
// clang-format on
});

int
pyproxy_init()
{
asyncio = PyImport_ImportModule("asyncio");
if (asyncio == NULL) {
return -1;
}
if (PyType_Ready(&FutureDoneCallbackType)) {
return -1;
}
if (pyproxy_init_js()) {
return -1;
}
return 0;
}
55 changes: 55 additions & 0 deletions src/core/python2js.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,49 @@ _Py_IDENTIFIER(format_exception);
static JsRef
_python2js_unicode(PyObject* x);

EM_JS_REF(JsRef, pyproxy_to_js_error, (JsRef pyproxy), {
return Module.hiwire.new_value(
new Module.PythonError(Module.hiwire.get_value(pyproxy)));
});

JsRef
wrap_exception()
{
bool success = true;
PyObject* type = NULL;
PyObject* value = NULL;
PyObject* traceback = NULL;
JsRef pyexc_proxy = NULL;
JsRef jserror = NULL;

PyErr_Fetch(&type, &value, &traceback);
PyErr_NormalizeException(&type, &value, &traceback);
if (type == NULL || type == Py_None || value == NULL || value == Py_None) {
PyErr_SetString(PyExc_TypeError, "No exception type or value");
FAIL();
}

if (traceback == NULL) {
traceback = Py_None;
Py_INCREF(traceback);
}
PyException_SetTraceback(value, traceback);

pyexc_proxy = pyproxy_new(value);
jserror = pyproxy_to_js_error(pyexc_proxy);

success = true;
finally:
Py_CLEAR(type);
Py_CLEAR(value);
Py_CLEAR(traceback);
hiwire_CLEAR(pyexc_proxy);
if (!success) {
hiwire_CLEAR(jserror);
}
return jserror;
}

void _Py_NO_RETURN
pythonexc2js()
{
Expand Down Expand Up @@ -393,6 +436,18 @@ python2js_init()
FAIL_IF_NULL(globals);

EM_ASM({
class PythonError extends Error
{
constructor(pythonError)
{
let message = "Python Error";
super(message);
this.name = this.constructor.name;
this.pythonError = pythonError;
}
};
Module.PythonError = PythonError;

Module.test_python2js_with_depth = function(name, depth)
{
let pyname = stringToNewUTF8(name);
Expand Down
3 changes: 3 additions & 0 deletions src/core/python2js.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
// clang-format on
#include "hiwire.h"

JsRef
wrap_exception();

/** Convert the active Python exception into a Javascript Error object
* and print it to the console.
*/
Expand Down