Skip to content

Commit 083ed51

Browse files
committed
Add PyProxy dict support
1 parent 6b3d954 commit 083ed51

3 files changed

Lines changed: 170 additions & 9 deletions

File tree

Lib/test/test_pyodide.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,34 @@ class T:
397397
)
398398
self.assertGreaterEqual(l, {"a", "b"})
399399

400+
def test_pyproxy_dict(self):
401+
d = {"a": 7, "b": 2, 3: "99"}
402+
l = set(
403+
x
404+
for x in run_js("(o) => Reflect.ownKeys(o)")(d)
405+
if isinstance(x, str)
406+
)
407+
self.assertGreaterEqual(l, {"a", "b", "3"})
408+
409+
self.assertEqual(run_js("(o) => o.get('a')")(d), 7)
410+
self.assertEqual(run_js("(o) => o.get(3)")(d), "99")
411+
self.assertEqual(run_js("(o) => o.get('3')")(d), None)
412+
self.assertEqual(run_js("(o) => o.a")(d), 7)
413+
self.assertEqual(run_js("(o) => o['a']")(d), 7)
414+
self.assertEqual(run_js("(o) => o[3]")(d), "99")
415+
416+
run_js("(o) => delete o[3]")(d)
417+
self.assertNotIn(3, d)
418+
run_js("(o) => o.delete('b')")(d)
419+
self.assertNotIn("b", d)
420+
run_js("(o) => o.z = 9")(d)
421+
self.assertEqual(d["z"], 9)
422+
run_js("(o) => o.set('q', 32)")(d)
423+
self.assertEqual(d["q"], 32)
424+
425+
# TODO:
426+
# self.assertEqual(list(run_js("(o) => o.items()")(d)), [])
427+
400428
def test_pyproxy_call_simple(self):
401429
def f(x):
402430
return x * x + 7
@@ -500,9 +528,7 @@ def test_pyproxy_set_del(self):
500528

501529
def test_pyproxy_iterable(self):
502530
d = [7, 21, 39]
503-
res = list(
504-
run_js("(d) => Array.from(d)")(d)
505-
)
531+
res = list(run_js("(d) => Array.from(d)")(d))
506532
self.assertEqual(res, d)
507533

508534
def test_pyproxy_iterator(self):

Modules/_pyodide_core/pyproxy.c

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

@@ -82,6 +83,7 @@ type_getflags(PyTypeObject* obj_type)
8283
SET_FLAG_IF(HAS_LENGTH, seq_proto->sq_length || map_proto->mp_length);
8384
SET_FLAG_IF(HAS_SET, map_proto->mp_ass_subscript || seq_proto->sq_ass_item);
8485
SET_FLAG_IF(IS_CALLABLE, obj_type->tp_call);
86+
SET_FLAG_IF(IS_DICT, Py_Is(obj_type, &PyDict_Type));
8587
SET_FLAG_IF(IS_ITERABLE, obj_type->tp_iter || seq_proto->sq_item);
8688

8789
extern PyObject* _PyObject_NextNotImplemented(PyObject *);

Modules/_pyodide_core/pyproxy.ts

Lines changed: 139 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,10 @@ declare function __pyproxyGen_Send(ptr: number, arg: any): IteratorResult<any>
5252
// This also has the benefit that it makes intellisense happy.
5353
declare var HAS_CONTAINS: number;
5454
declare var HAS_GET: number;
55-
declare var HAS_HAS: number;
56-
declare var HAS_INCLUDES: number;
5755
declare var HAS_LENGTH: number;
5856
declare var HAS_SET: number;
59-
declare var IS_ARRAY: number;
6057
declare var IS_CALLABLE: number;
61-
declare var IS_ERROR: number;
62-
declare var IS_GENERATOR: number;
58+
declare var IS_DICT: number;
6359
declare var IS_ITERABLE: number;
6460
declare var IS_ITERATOR: number;
6561

@@ -187,6 +183,7 @@ function pyproxy_new(
187183
if (flags === -1) {
188184
_pythonexc2js();
189185
}
186+
const is_dict = flags & IS_DICT;
190187
const cls = getPyProxyClass(flags);
191188
let target: any;
192189
if (flags & IS_CALLABLE) {
@@ -234,7 +231,11 @@ function pyproxy_new(
234231
props,
235232
);
236233
let handlers;
237-
handlers = PyProxyHandlers;
234+
if (is_dict) {
235+
handlers = PyProxyDictHandlers;
236+
} else {
237+
handlers = PyProxyHandlers;
238+
}
238239
let proxy = new Proxy(target, handlers);
239240
if (!isAlias && gcRegister) {
240241
// we need to register only once for a set of aliases. we can't register the
@@ -583,6 +584,138 @@ const PyProxyHandlers = {
583584
};
584585

585586

587+
588+
const PyProxyDictHandlersSet = new Set([
589+
"copy",
590+
"constructor",
591+
"$$flags",
592+
"toString",
593+
"destroy",
594+
]);
595+
596+
interface PythonError {
597+
type: string;
598+
}
599+
600+
function isPythonError(e: any): e is PythonError {
601+
return (
602+
e &&
603+
typeof e === "object" &&
604+
e.constructor &&
605+
e.constructor.name === "PythonError"
606+
);
607+
}
608+
609+
const PyProxyDictHandlers = {
610+
isExtensible(): boolean {
611+
return true;
612+
},
613+
has(jsobj: PyProxy, jskey: string | symbol): boolean {
614+
if (PyContainsMethods.prototype.has.call(jsobj, jskey)) {
615+
return true;
616+
}
617+
if (typeof jskey === "string" && /^[0-9]+$/.test(jskey)) {
618+
return PyContainsMethods.prototype.has.call(jsobj, Number(jskey));
619+
}
620+
return false;
621+
},
622+
get(jsobj: PyProxy, jskey: string | symbol): any {
623+
if (
624+
typeof jskey === "symbol" ||
625+
PyProxyDictHandlersSet.has(jskey)
626+
) {
627+
// @ts-ignore
628+
return Reflect.get(...arguments);
629+
}
630+
const result = PyGetItemMethods.prototype.get.call(jsobj, jskey);
631+
if (
632+
result !== undefined ||
633+
PyContainsMethods.prototype.has.call(jsobj, jskey)
634+
) {
635+
return result;
636+
}
637+
if (typeof jskey === "string" && /^[0-9]+$/.test(jskey)) {
638+
return PyGetItemMethods.prototype.get.call(jsobj, Number(jskey));
639+
}
640+
// @ts-ignore
641+
return Reflect.get(...arguments);
642+
},
643+
set(jsobj: PyProxy, jskey: string | symbol | number, jsval: any): boolean {
644+
if (typeof jskey === "symbol") {
645+
return false;
646+
}
647+
if (
648+
!PyContainsMethods.prototype.has.call(jsobj, jskey) &&
649+
typeof jskey === "string" &&
650+
/^[0-9]+$/.test(jskey)
651+
) {
652+
jskey = Number(jskey);
653+
}
654+
try {
655+
PySetItemMethods.prototype.set.call(jsobj, jskey, jsval);
656+
return true;
657+
} catch (e) {
658+
if (isPythonError(e) && e.type === "KeyError") {
659+
return false;
660+
}
661+
throw e;
662+
}
663+
},
664+
deleteProperty(jsobj: PyProxy, jskey: string | symbol | number): boolean {
665+
if (typeof jskey === "symbol") {
666+
return false;
667+
}
668+
if (
669+
!PyContainsMethods.prototype.has.call(jsobj, jskey) &&
670+
typeof jskey === "string" &&
671+
/^[0-9]+$/.test(jskey)
672+
) {
673+
jskey = Number(jskey);
674+
}
675+
try {
676+
PySetItemMethods.prototype.delete.call(jsobj, jskey);
677+
return true;
678+
} catch (e) {
679+
if (isPythonError(e) && e.type === "KeyError") {
680+
return false;
681+
}
682+
throw e;
683+
}
684+
},
685+
getOwnPropertyDescriptor(jsobj: PyProxy, prop: any) {
686+
if (!PyProxyDictHandlers.has(jsobj, prop)) {
687+
return undefined;
688+
}
689+
const value = PyProxyDictHandlers.get(jsobj, prop);
690+
return {
691+
configurable: true,
692+
enumerable: true,
693+
value,
694+
writable: true,
695+
};
696+
},
697+
ownKeys(jsobj: PyProxy): (string | symbol)[] {
698+
const result: Set<string | symbol> = new Set();
699+
dictOwnKeysHelper(jsobj, result);
700+
return Array.from(result);
701+
},
702+
};
703+
704+
function dictOwnKeysHelper(jsobj: PyProxy, result: Set<string | symbol>): void {
705+
const dictKeysView: Iterable<any> & PyProxy = PyProxyHandlers.get(
706+
jsobj,
707+
"keys",
708+
)();
709+
for (const key of dictKeysView) {
710+
if (typeof key === "string") {
711+
result.add(key);
712+
} else if (typeof key === "number") {
713+
result.add(key.toString());
714+
}
715+
}
716+
dictKeysView.destroy();
717+
}
718+
586719
// Another layer of boilerplate. The PyProxyHandlers have some annoying logic to
587720
// deal with straining out the spurious "Function" properties "prototype",
588721
// "arguments", and "length", to deal with correctly satisfying the Proxy

0 commit comments

Comments
 (0)