Skip to content

Commit 5665082

Browse files
committed
Add PyProxy iterator support
1 parent 30cd06c commit 5665082

3 files changed

Lines changed: 115 additions & 1 deletion

File tree

Lib/test/test_pyodide.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,11 @@ class T:
390390
a = 7
391391
b = "zz"
392392

393-
l = set(x for x in run_js("(o) => Reflect.ownKeys(o)")(T) if isinstance(x, str))
393+
l = set(
394+
x
395+
for x in run_js("(o) => Reflect.ownKeys(o)")(T)
396+
if isinstance(x, str)
397+
)
394398
self.assertGreaterEqual(l, {"a", "b"})
395399

396400
def test_pyproxy_call_simple(self):
@@ -493,3 +497,17 @@ def test_pyproxy_set_del(self):
493497
self.assertEqual(d["y"], 3)
494498
run_js("(d) => d.delete('x')")(d)
495499
self.assertNotIn("x", d)
500+
501+
def test_pyproxy_iterator(self):
502+
d = [7, 21, 39]
503+
res = list(
504+
run_js("(d) => [d.next(), d.next(), d.next(), d.next()]")(iter(d))
505+
)
506+
self.assertFalse(res[0].done)
507+
self.assertFalse(res[1].done)
508+
self.assertFalse(res[2].done)
509+
self.assertTrue(res[3].done)
510+
self.assertEqual(res[0].value, 7)
511+
self.assertEqual(res[1].value, 21)
512+
self.assertEqual(res[2].value, 39)
513+
self.assertEqual(res[3].value, None)

Modules/_pyodide_core/pyproxy.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#define HAS_LENGTH (1 << 2)
1313
#define HAS_SET (1 << 3)
1414
#define IS_CALLABLE (1 << 4)
15+
#define IS_ITERABLE (1 << 7)
16+
#define IS_ITERATOR (1 << 8)
1517

1618
EM_JS_VAL(JsVal, pyproxy_new, (PyObject * ptrobj), {
1719
return Module.pyproxy_new(ptrobj);
@@ -80,6 +82,15 @@ type_getflags(PyTypeObject* obj_type)
8082
SET_FLAG_IF(HAS_LENGTH, seq_proto->sq_length || map_proto->mp_length);
8183
SET_FLAG_IF(HAS_SET, map_proto->mp_ass_subscript || seq_proto->sq_ass_item);
8284
SET_FLAG_IF(IS_CALLABLE, obj_type->tp_call);
85+
SET_FLAG_IF(IS_ITERABLE, obj_type->tp_iter || seq_proto->sq_item);
86+
87+
extern PyObject* _PyObject_NextNotImplemented(PyObject *);
88+
if (obj_type->tp_iternext != NULL &&
89+
obj_type->tp_iternext != &_PyObject_NextNotImplemented) {
90+
result &= ~IS_ITERABLE;
91+
result |= IS_ITERATOR;
92+
}
93+
8394
return result;
8495

8596
#undef SET_FLAG_IF
@@ -431,3 +442,32 @@ _pyproxy_apply(PyObject* callable,
431442
return result;
432443
}
433444

445+
EM_JS(JsVal, _pyproxyGen_make_result, (bool done, JsVal value), {
446+
return { done : !!done, value };
447+
})
448+
449+
EMSCRIPTEN_KEEPALIVE JsVal
450+
_pyproxyGen_Send(PyObject* receiver, JsVal jsval)
451+
{
452+
bool success = false;
453+
PyObject* v = NULL;
454+
PyObject* retval = NULL;
455+
456+
v = js2python(jsval);
457+
FAIL_IF_NULL(v);
458+
PySendResult status = PyIter_Send(receiver, v, &retval);
459+
if (status == PYGEN_ERROR) {
460+
FAIL();
461+
}
462+
JsVal result = python2js(retval);
463+
FAIL_IF_JS_ERROR(result);
464+
465+
success = true;
466+
finally:
467+
Py_CLEAR(v);
468+
Py_CLEAR(retval);
469+
if (!success) {
470+
return JS_ERROR;
471+
}
472+
return _pyproxyGen_make_result(status == PYGEN_RETURN, result);
473+
}

Modules/_pyodide_core/pyproxy.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ declare function __pyproxy_apply(
3434
kwargs_names: string[],
3535
num_kwargs: number,
3636
): any;
37+
declare function __pyproxyGen_Send(ptr: number, arg: any): IteratorResult<any>
3738

3839
// pyodide-skip
3940

@@ -298,6 +299,7 @@ function getPyProxyClass(flags: number) {
298299
[HAS_LENGTH, PyLengthMethods],
299300
[HAS_SET, PySetItemMethods],
300301
[IS_CALLABLE, PyCallableMethods],
302+
[IS_ITERATOR, PyIteratorMethods],
301303
];
302304
for (let [feature_flag, methods] of FLAG_TYPE_PAIRS) {
303305
if (flags & feature_flag) {
@@ -1038,3 +1040,57 @@ function callPyObjectKwargs(ptrobj: number, jsargs: any[], kwargs: any) {
10381040
function callPyObject(ptrobj: number, jsargs: any) {
10391041
return callPyObjectKwargs(ptrobj, jsargs, {});
10401042
}
1043+
1044+
1045+
/**
1046+
* A :js:class:`~pyodide.ffi.PyProxy` whose proxied Python object is an :term:`iterator`
1047+
* (i.e., has a :meth:`~generator.send` or :meth:`~iterator.__next__` method).
1048+
*/
1049+
class PyIterator extends PyProxy {
1050+
/** @private */
1051+
static [Symbol.hasInstance](obj: any): obj is PyProxy {
1052+
return API.isPyProxy(obj) && !!(_getFlags(obj) & IS_ITERATOR);
1053+
}
1054+
}
1055+
1056+
interface PyIterator extends PyIteratorMethods {}
1057+
1058+
// Controlled by IS_ITERATOR, appears for any object with a __next__ or
1059+
// tp_iternext method.
1060+
class PyIteratorMethods {
1061+
/** @private */
1062+
[Symbol.iterator]() {
1063+
return this;
1064+
}
1065+
/**
1066+
* This translates to the Python code ``next(obj)``. Returns the next value of
1067+
* the generator. See the documentation for :js:meth:`Generator.next` The
1068+
* argument will be sent to the Python generator.
1069+
*
1070+
* This will be used implicitly by ``for(let x of proxy){}``.
1071+
*
1072+
* @param arg The value to send to the generator. The value will be assigned
1073+
* as a result of a yield expression.
1074+
* @returns An Object with two properties: ``done`` and ``value``. When the
1075+
* generator yields ``some_value``, ``next`` returns ``{done : false, value :
1076+
* some_value}``. When the generator raises a :py:exc:`StopIteration`
1077+
* exception, ``next`` returns ``{done : true, value : result_value}``.
1078+
*/
1079+
next(arg: any = undefined): IteratorResult<any, any> {
1080+
// Note: arg is optional, if arg is not supplied, it will be undefined
1081+
// which gets converted to "Py_None". This is as intended.
1082+
let result;
1083+
let done;
1084+
try {
1085+
Py_ENTER();
1086+
result = __pyproxyGen_Send(_getPtr(this), arg);
1087+
Py_EXIT();
1088+
} catch (e) {
1089+
API.fatal_error(e);
1090+
}
1091+
if (result === Module.error) {
1092+
_pythonexc2js();
1093+
}
1094+
return result;
1095+
}
1096+
}

0 commit comments

Comments
 (0)