@@ -9,7 +9,7 @@ declare function _Py_IncRef(ptr: number): void;
99declare function _Py_DecRef ( ptr : number ) : void ;
1010declare function _PyErr_Occurred ( ) : number ;
1111declare function _PyObject_Size ( ptr : number ) : number ;
12-
12+ declare function _PyObject_GetIter ( ptr : number ) : number ;
1313
1414
1515declare 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 ;
3738declare 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).
0 commit comments