Skip to content

Commit a5da7d8

Browse files
committed
Add context manager support for JsProxies with Symbol.dispose
1 parent 1a1dcfa commit a5da7d8

4 files changed

Lines changed: 151 additions & 11 deletions

File tree

Lib/test/test_pyodide.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,34 @@ def test_instance_checks(self):
841841
self.assertIsInstance(p, JsDoubleProxy)
842842
p.destroy()
843843

844+
def test_jsproxy_context_manager(self):
845+
f = run_js(
846+
"""
847+
(function f() {
848+
return {
849+
x: 9,
850+
closed: false,
851+
[Symbol.dispose] () {
852+
this.closed = true;
853+
}
854+
};
855+
})
856+
"""
857+
)
858+
with f() as o:
859+
self.assertEqual(o.x, 9)
860+
self.assertFalse(o.closed)
861+
self.assertTrue(o.closed)
862+
863+
try:
864+
with f() as o:
865+
self.assertEqual(o.x, 9)
866+
self.assertFalse(o.closed)
867+
raise Exception("oops")
868+
except Exception:
869+
pass
870+
self.assertTrue(o.closed)
871+
844872

845873
class PyProxyTest(TestCase):
846874
def test_pyproxy(self):

Modules/_pyodide_core/clinic/jsproxy.c.h

Lines changed: 62 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_pyodide_core/jsproxy.c

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@
2323
#define HAS_INCLUDES (1 << 2)
2424
#define HAS_LENGTH (1 << 3)
2525
#define HAS_SET (1 << 4)
26-
#define IS_ARRAY (1 << 5)
27-
#define IS_CALLABLE (1 << 6)
28-
#define IS_ERROR (1 << 7)
29-
#define IS_GENERATOR (1 << 8)
30-
#define IS_ITERABLE (1 << 9)
31-
#define IS_ITERATOR (1 << 10)
32-
#define IS_DOUBLE_PROXY (1 << 11)
33-
#define IS_PY_JSON_DICT (1 << 12)
34-
#define IS_PY_JSON_SEQUENCE (1 << 13)
26+
#define HAS_DISPOSE (1 << 5)
27+
#define IS_ARRAY (1 << 6)
28+
#define IS_CALLABLE (1 << 7)
29+
#define IS_ERROR (1 << 8)
30+
#define IS_GENERATOR (1 << 9)
31+
#define IS_ITERABLE (1 << 10)
32+
#define IS_ITERATOR (1 << 11)
33+
#define IS_DOUBLE_PROXY (1 << 12)
34+
#define IS_PY_JSON_DICT (1 << 13)
35+
#define IS_PY_JSON_SEQUENCE (1 << 14)
3536

3637
Js_IDENTIFIER(next);
3738

@@ -949,6 +950,47 @@ JsProxy_includes(JsProxy* self, PyObject* obj)
949950
return result;
950951
}
951952

953+
954+
/*[clinic input]
955+
_pyodide_core.JsProxy.__enter__
956+
957+
If [Symbol.dispose] is present, implement context manager protocol.
958+
[clinic start generated code]*/
959+
960+
static PyObject *
961+
_pyodide_core_JsProxy___enter___impl(PyObject *self)
962+
/*[clinic end generated code: output=7d0abcbd81673f6b input=3ab21922f5faedaf]*/
963+
{
964+
return Py_NewRef(self);
965+
}
966+
967+
EM_JS_NUM(int, JsProxy_exit_js, (JsVal obj), {
968+
obj[Symbol.dispose]();
969+
});
970+
971+
/*[clinic input]
972+
_pyodide_core.JsProxy.__exit__
973+
974+
exc_type: object = None
975+
exc_value: object = None
976+
exc_tb: object = None
977+
/
978+
979+
If [Symbol.dispose] is present, implement context manager protocol.
980+
[clinic start generated code]*/
981+
982+
static PyObject *
983+
_pyodide_core_JsProxy___exit___impl(PyObject *self, PyObject *exc_type,
984+
PyObject *exc_value, PyObject *exc_tb)
985+
/*[clinic end generated code: output=e52ec221b82c0b6f input=40df97659f01cc5a]*/
986+
{
987+
if (JsProxy_exit_js(JsProxy_VAL(self)) == -1) {
988+
return NULL;
989+
}
990+
Py_RETURN_NONE;
991+
}
992+
993+
952994
EM_JS_VAL(JsVal, _PyJsMap_GetIter_js, (JsVal obj), {
953995
let result;
954996
// clang-format off
@@ -2425,6 +2467,12 @@ JsProxy_create_subtype(int flags, bool is_py_json)
24252467
slots[cur_slot++] =
24262468
(PyType_Slot){ .slot = Py_mp_length, .pfunc = (void*)JsProxy_length };
24272469
}
2470+
if (flags & HAS_DISPOSE) {
2471+
AddMethods(
2472+
_PYODIDE_CORE_JSPROXY___ENTER___METHODDEF
2473+
_PYODIDE_CORE_JSPROXY___EXIT___METHODDEF
2474+
);
2475+
}
24282476

24292477
if (flags & IS_ARRAY) {
24302478
// If the object is an array (or a HTMLCollection or NodeList), then we want
@@ -2691,6 +2739,9 @@ EM_JS_NUM(int, JsProxy_compute_typeflags, (JsVal obj, bool is_py_json), {
26912739
SET_FLAG_IF(HAS_LENGTH,
26922740
(hasProperty(obj, "size")) ||
26932741
(hasProperty(obj, "length") && typeof obj !== "function"));
2742+
if (Symbol.asyncDispose) {
2743+
SET_FLAG_IF_HAS_METHOD(HAS_DISPOSE, Symbol.dispose);
2744+
}
26942745
SET_FLAG_IF(IS_CALLABLE, typeof obj === "function");
26952746
SET_FLAG_IF(IS_ARRAY, safeCall(() => Array.isArray(obj)));
26962747
SET_FLAG_IF(IS_DOUBLE_PROXY, API.isPyProxy(obj));

Modules/_pyodide_core/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"strict": true,
1111
"noUnusedLocals": false,
1212
"experimentalDecorators": true,
13-
"lib": ["ES2022", "DOM"],
13+
"lib": ["ES2022", "DOM", "ESNext.Disposable"],
1414
},
1515
"include": ["*.ts", "*.json"],
1616
}

0 commit comments

Comments
 (0)