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

PyProxy methods for iterators / iterables / generators #1180

Merged
merged 8 commits into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
7 changes: 7 additions & 0 deletions src/core/hiwire.c
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ EM_JS(int, hiwire_init, (), {
_hiwire.objects.delete(idval);
};

Module.hiwire.pop_value = function(idval)
{
let result = Module.hiwire.get_value(idval);
Module.hiwire.decref(idval);
return result;
};

Module.hiwire.isPromise = function(obj)
{
// clang-format off
Expand Down
162 changes: 145 additions & 17 deletions src/core/pyproxy.c
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,84 @@ _pyproxy_apply(PyObject* pyobj, JsRef idargs)
return idresult;
}

// Return 2 if obj is iterator
// Return 1 if iterable but not iterator
// Return 0 if not iterable
int
_pyproxy_iterator_type(PyObject* obj)
{
if (PyIter_Check(obj)) {
return 2;
}
PyObject* iter = PyObject_GetIter(obj);
int result = iter != NULL;
Py_CLEAR(iter);
PyErr_Clear();
return result;
}

JsRef
_pyproxy_iter_next(PyObject* iterator)
{
PyObject* item = PyIter_Next(iterator);
if (item == NULL) {
return NULL;
}
JsRef result = python2js(item);
Py_CLEAR(item);
return result;
}

JsRef
_pyproxy_iter_send(PyObject* receiver, JsRef jsval)
{
bool success = false;
PyObject* v = NULL;
PyObject* retval = NULL;
JsRef jsresult = NULL;

// cf implementation of YIELD_FROM opcode in ceval.c
v = js2python(jsval);
FAIL_IF_NULL(v);
if (PyGen_CheckExact(receiver) || PyCoro_CheckExact(receiver)) {
retval = _PyGen_Send((PyGenObject*)receiver, v);
} else if (v == Py_None) {
retval = Py_TYPE(receiver)->tp_iternext(receiver);
} else {
_Py_IDENTIFIER(send);
retval = _PyObject_CallMethodIdObjArgs(receiver, &PyId_send, v, NULL);
}
FAIL_IF_NULL(retval);

jsresult = python2js(retval);
FAIL_IF_NULL(jsresult);

success = true;
finally:
Py_CLEAR(v);
Py_CLEAR(retval);
if (!success) {
hiwire_CLEAR(jsresult);
}
return jsresult;
}

JsRef
_pyproxy_iter_fetch_stopiteration()
{
PyObject* val = NULL;
// cf implementation of YIELD_FROM opcode in ceval.c
// _PyGen_FetchStopIterationValue returns an error code, but it seems
// redundant
_PyGen_FetchStopIterationValue(&val);
if (val == NULL) {
return NULL;
}
JsRef result = python2js(val);
Py_CLEAR(val);
return result;
}

void
_pyproxy_destroy(PyObject* ptrobj)
{ // See bug #1049
Expand All @@ -161,6 +239,15 @@ EM_JS_REF(JsRef, pyproxy_new, (PyObject * ptrobj), {
target['$$'] = { ptr : ptrobj, type : 'PyProxy' };
Object.assign(target, Module.PyProxyPublicMethods);
let proxy = new Proxy(target, Module.PyProxyHandlers);
let itertype = __pyproxy_iterator_type(ptrobj);
// clang-format off
if (itertype === 2) {
Object.assign(target, Module.PyProxyIteratorMethods);
}
if (itertype === 1) {
Object.assign(target, Module.PyProxyIterableMethods);
}
// clang-format on
Module.PyProxies[ptrobj] = proxy;

return Module.hiwire.new_value(proxy);
Expand Down Expand Up @@ -196,9 +283,7 @@ EM_JS(int, pyproxy_init, (), {
if(jsref_repr === 0){
_pythonexc2js();
}
let repr = Module.hiwire.get_value(jsref_repr);
Module.hiwire.decref(jsref_repr);
return repr;
return Module.hiwire.pop_value(jsref_repr);
},
destroy : function() {
let ptrobj = _getPtr(this);
Expand All @@ -219,9 +304,59 @@ EM_JS(int, pyproxy_init, (), {
if(idresult === 0){
_pythonexc2js();
}
let jsresult = Module.hiwire.get_value(idresult);
Module.hiwire.decref(idresult);
return jsresult;
return Module.hiwire.pop_value(idresult);
},
};

// See:
// https://docs.python.org/3/c-api/iter.html
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
// This avoids allocating a PyProxy wrapper for the temporary iterator.
Module.PyProxyIterableMethods = {
[Symbol.iterator] : function*() {
let iterptr = _PyObject_GetIter(_getPtr(this));
if(iterptr === 0){
pythonexc2js();
}
let item;
while((item = __pyproxy_iter_next(iterptr))){
yield Module.hiwire.pop_value(item);
}
if(_PyErr_Occurred()){
pythonexc2js();
}
_Py_DecRef(iterptr);
}
};

Module.PyProxyIteratorMethods = {
[Symbol.iterator] : function() {
return this;
},
next : function(arg) {
let idresult;
// Note: arg is optional, if arg is not supplied, it will be undefined
// which gets converted to "Py_None". This is as intended.
let idarg = Module.hiwire.new_value(arg);
try {
idresult = __pyproxy_iter_send(_getPtr(this), idarg);
} catch(e) {
Module.fatal_error(e);
} finally {
Module.hiwire.decref(idarg);
}

let done = false;
if(idresult === 0){
idresult = __pyproxy_iter_fetch_stopiteration();
if (idresult){
done = true;
} else {
_pythonexc2js();
}
}
let value = Module.hiwire.pop_value(idresult);
return { done, value };
},
};

Expand Down Expand Up @@ -267,9 +402,7 @@ EM_JS(int, pyproxy_init, (), {
if(idresult === 0){
_pythonexc2js();
}
let jsresult = Module.hiwire.get_value(idresult);
Module.hiwire.decref(idresult);
return jsresult;
return Module.hiwire.pop_value(idresult);
},
set: function (jsobj, jskey, jsval) {
if(Reflect.has(jsobj, jskey) && !ignoredTargetFields.includes(jskey)){
Expand All @@ -290,9 +423,7 @@ EM_JS(int, pyproxy_init, (), {
if(idresult === 0){
_pythonexc2js();
}
let jsresult = Module.hiwire.get_value(idresult);
Module.hiwire.decref(idresult);
return jsresult;
return Module.hiwire.pop_value(idresult);
},
deleteProperty: function (jsobj, jskey) {
if(Reflect.has(jsobj, jskey) && !ignoredTargetFields.includes(jskey)){
Expand All @@ -311,9 +442,7 @@ EM_JS(int, pyproxy_init, (), {
if(idresult === 0){
_pythonexc2js();
}
let jsresult = Module.hiwire.get_value(idresult);
Module.hiwire.decref(idresult);
return jsresult;
return Module.hiwire.pop_value(idresult);
},
ownKeys: function (jsobj) {
let result = new Set(Reflect.ownKeys(jsobj));
Expand All @@ -327,8 +456,7 @@ EM_JS(int, pyproxy_init, (), {
} catch(e) {
Module.fatal_error(e);
}
let jsresult = Module.hiwire.get_value(idresult);
Module.hiwire.decref(idresult);
let jsresult = Module.hiwire.pop_value(idresult);
for(let key of jsresult){
result.add(key);
}
Expand Down
68 changes: 68 additions & 0 deletions src/tests/test_pyproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,71 @@ def get_value(self, value):
f.get_value();
"""
)


def test_pyproxy_iter(selenium):
assert (
selenium.run_js(
"""
let c = pyodide.runPython(`
def test():
for i in range(10):
yield i
test()
`);
return [...c];
"""
)
== list(range(10))
)

assert (
set(
selenium.run_js(
"""
c = pyodide.runPython(`
from collections import ChainMap
ChainMap({"a" : 2, "b" : 3})
`);
return [...c];
"""
)
)
== set(["a", "b"])
)

[result, result2] = selenium.run_js(
"""
let c = pyodide.runPython(`
def test():
acc = 0
for i in range(10):
r = yield acc
acc += i * r
test()
`)
let {done, value} = c.next();
let result = [];
while(!done){
result.push(value);
({done, value} = c.next(value + 1));
}

function* test(){
let acc = 0;
for(let i=0; i < 10; i++){
let r = yield acc;
acc += i * r;
}
}
c = test();
({done, value} = c.next());
let result2 = [];
while(!done){
result2.push(value);
({done, value} = c.next(value + 1));
}
return [result, result2];
"""
)
assert result == result2