Skip to content

Commit 6b3d954

Browse files
committed
Add PyProxy iterable support
1 parent 5665082 commit 6b3d954

5 files changed

Lines changed: 132 additions & 1 deletion

File tree

Lib/test/test_pyodide.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,13 @@ def test_pyproxy_set_del(self):
498498
run_js("(d) => d.delete('x')")(d)
499499
self.assertNotIn("x", d)
500500

501+
def test_pyproxy_iterable(self):
502+
d = [7, 21, 39]
503+
res = list(
504+
run_js("(d) => Array.from(d)")(d)
505+
)
506+
self.assertEqual(res, d)
507+
501508
def test_pyproxy_iterator(self):
502509
d = [7, 21, 39]
503510
res = list(

Modules/_pyodide_core/pyproxy.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,18 @@ _pyproxy_apply(PyObject* callable,
442442
return result;
443443
}
444444

445+
EMSCRIPTEN_KEEPALIVE JsVal
446+
_pyproxy_iter_next(PyObject* iterator)
447+
{
448+
PyObject* item = PyIter_Next(iterator);
449+
if (item == NULL) {
450+
return JS_ERROR;
451+
}
452+
JsVal result = python2js(item);
453+
Py_CLEAR(item);
454+
return result;
455+
}
456+
445457
EM_JS(JsVal, _pyproxyGen_make_result, (bool done, JsVal value), {
446458
return { done : !!done, value };
447459
})

Modules/_pyodide_core/pyproxy.ts

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ declare function _Py_IncRef(ptr: number): void;
99
declare function _Py_DecRef(ptr: number): void;
1010
declare function _PyErr_Occurred(): number;
1111
declare function _PyObject_Size(ptr: number): number;
12-
12+
declare function _PyObject_GetIter(ptr: number): number;
1313

1414

1515
declare function _pythonexc2js(): never;
@@ -34,6 +34,7 @@ declare function __pyproxy_apply(
3434
kwargs_names: string[],
3535
num_kwargs: number,
3636
): any;
37+
declare function __pyproxy_iter_next(ptr: number): any;
3738
declare function __pyproxyGen_Send(ptr: number, arg: any): IteratorResult<any>
3839

3940
// pyodide-skip
@@ -299,6 +300,7 @@ function getPyProxyClass(flags: number) {
299300
[HAS_LENGTH, PyLengthMethods],
300301
[HAS_SET, PySetItemMethods],
301302
[IS_CALLABLE, PyCallableMethods],
303+
[IS_ITERABLE, PyIterableMethods],
302304
[IS_ITERATOR, PyIteratorMethods],
303305
];
304306
for (let [feature_flag, methods] of FLAG_TYPE_PAIRS) {
@@ -1042,6 +1044,112 @@ function callPyObject(ptrobj: number, jsargs: any) {
10421044
}
10431045

10441046

1047+
/**
1048+
* A helper for [Symbol.iterator].
1049+
*
1050+
* Because "it is possible for a generator to be garbage collected without
1051+
* ever running its finally block", we take extra care to try to ensure that
1052+
* we don't leak the iterator. We register it with the finalizationRegistry,
1053+
* but if the finally block is executed, we decref the pointer and unregister.
1054+
*
1055+
* In order to do this, we create the generator with this inner method,
1056+
* register the finalizer, and then return it.
1057+
*
1058+
* Quote from:
1059+
* https://hacks.mozilla.org/2015/07/es6-in-depth-generators-continued/
1060+
*
1061+
*/
1062+
function* iter_helper(
1063+
iterptr: number,
1064+
token: {},
1065+
): Generator<any> {
1066+
const to_destroy = [];
1067+
try {
1068+
while (true) {
1069+
Py_ENTER();
1070+
const item = __pyproxy_iter_next(iterptr);
1071+
Py_EXIT();
1072+
if (item === Module.error) {
1073+
break;
1074+
}
1075+
yield item;
1076+
// This is necessary to get JSON.stringify to work correctly.
1077+
if (API.isPyProxy(item)) {
1078+
to_destroy.push(item);
1079+
}
1080+
}
1081+
} catch (e) {
1082+
API.fatal_error(e);
1083+
} finally {
1084+
Module.finalizationRegistry.unregister(token);
1085+
_Py_DecRef(iterptr);
1086+
}
1087+
try {
1088+
to_destroy.forEach((e) =>
1089+
Module.pyproxy_destroy(
1090+
e,
1091+
"This borrowed proxy was automatically destroyed when an iterator was exhausted.",
1092+
),
1093+
);
1094+
} catch (e) {}
1095+
if (_PyErr_Occurred()) {
1096+
_pythonexc2js();
1097+
}
1098+
}
1099+
1100+
/**
1101+
* A :js:class:`~pyodide.ffi.PyProxy` whose proxied Python object is :std:term:`iterable`
1102+
* (i.e., it has an :meth:`~object.__iter__` method).
1103+
*/
1104+
export class PyIterable extends PyProxy {
1105+
/** @private */
1106+
static [Symbol.hasInstance](obj: any): obj is PyProxy {
1107+
return (
1108+
API.isPyProxy(obj) && !!(_getFlags(obj) & (IS_ITERABLE | IS_ITERATOR))
1109+
);
1110+
}
1111+
}
1112+
1113+
export interface PyIterable extends PyIterableMethods {}
1114+
1115+
// Controlled by IS_ITERABLE, appears for any object with __iter__ or tp_iter,
1116+
// unless they are iterators. See: https://docs.python.org/3/c-api/iter.html
1117+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
1118+
// This avoids allocating a PyProxy wrapper for the temporary iterator.
1119+
export class PyIterableMethods {
1120+
/**
1121+
* This translates to the Python code ``iter(obj)``. Return an iterator
1122+
* associated to the proxy. See the documentation for
1123+
* :js:data:`Symbol.iterator`.
1124+
*
1125+
* This will be used implicitly by ``for(let x of proxy){}``.
1126+
*/
1127+
[Symbol.iterator](): Iterator<any, any, any> {
1128+
const { shared } = _getAttrs(this);
1129+
let token = {};
1130+
let iterptr;
1131+
try {
1132+
Py_ENTER();
1133+
iterptr = _PyObject_GetIter(shared.ptr);
1134+
Py_EXIT();
1135+
} catch (e) {
1136+
API.fatal_error(e);
1137+
}
1138+
if (iterptr === 0) {
1139+
_pythonexc2js();
1140+
}
1141+
1142+
// Cache is only used if isJsonAdaptor is true.
1143+
let result = iter_helper(
1144+
iterptr,
1145+
token,
1146+
);
1147+
Module.finalizationRegistry.register(result, [iterptr, undefined], token);
1148+
return result;
1149+
}
1150+
}
1151+
1152+
10451153
/**
10461154
* A :js:class:`~pyodide.ffi.PyProxy` whose proxied Python object is an :term:`iterator`
10471155
* (i.e., has a :meth:`~generator.send` or :meth:`~iterator.__next__` method).

configure

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

configure.ac

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2351,6 +2351,8 @@ AS_CASE([$ac_sys_system],
23512351
AS_VAR_APPEND([LINKFORSHARED], ["_PyLongWriter_Create,"])
23522352
AS_VAR_APPEND([LINKFORSHARED], ["_PyLongWriter_Finish,"])
23532353
AS_VAR_APPEND([LINKFORSHARED], ["_PyObject_Size,"])
2354+
AS_VAR_APPEND([LINKFORSHARED], ["_PyObject_GetIter,"])
2355+
23542356
dnl some hiwire functions
23552357
AS_VAR_APPEND([LINKFORSHARED], ["_hiwire_intern,"])
23562358
AS_VAR_APPEND([LINKFORSHARED], ["_hiwire_get,"])

0 commit comments

Comments
 (0)