Skip to content

Commit

Permalink
webassembly/objjsproxy: Lookup attributes without testing they exist.
Browse files Browse the repository at this point in the history
In JavaScript when accessing an attribute such as `obj.attr` a value of
`undefined` is returned if the attribute does not exist.  This is unlike
Python semantics where an `AttributeError` is raised.  Furthermore, in some
cases in JavaScript (eg a Proxy instance) `attr in obj` can return false
yet `obj.attr` is still valid and returns something other than `undefined`.
So the source of truth for whether a JavaScript attribute exists is to just
right away attempt `obj.attr`.

To more closely match these JavaScript semantics when proxying a JavaScript
object through to Python, change the attribute lookup logic on a `JsProxy`
so that it immediately attempts `obj.attr` instead of first testing if the
attribute exists via `attr in obj`.

This allows JavaScript objects which dynamically create attributes to work
correctly on the Python side, with both `obj.attr` and `obj["attr"]`.  Note
that `obj["attr"]` already works in all cases because it immediately does
the subscript access without first testing if the attribute exists.

As a benefit, this new behaviour matches the Pyodide behaviour.

Signed-off-by: Damien George <damien@micropython.org>
  • Loading branch information
dpgeorge committed Jun 28, 2024
1 parent 5dff78f commit 95c19e0
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 2 deletions.
10 changes: 8 additions & 2 deletions ports/webassembly/objjsproxy.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@ EM_JS(bool, has_attr, (int jsref, const char *str), {
EM_JS(bool, lookup_attr, (int jsref, const char *str, uint32_t * out), {
const base = proxy_js_ref[jsref];
const attr = UTF8ToString(str);
if (attr in base) {
let value = base[attr];

// Attempt to lookup the requested attribute from the base object:
// - If the value is not `undefined` then the attribute exists with that value.
// - Otherwise if the value is `undefined` and the `in` operator returns true, then
// that attribute does exist and is intended to have a value of `undefined`.
// - Otherwise, the attribute does not exist.
let value = base[attr];
if (value !== undefined || attr in base) {
if (typeof value === "function") {
if (base !== globalThis) {
if ("_ref" in value) {
Expand Down
34 changes: 34 additions & 0 deletions tests/ports/webassembly/js_proxy_attribute.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Test lookup of attributes on JsProxy objects.

const mp = await (await import(process.argv[2])).loadMicroPython();

// Simple attribute names and values.
globalThis.obj1 = { a: 1, b: 2 };

// Unconventional attribute names and values.
globalThis.obj2 = { undefined: "undefined", undef: undefined };

// Dynamically created attribute names and values.
globalThis.obj3 = new Proxy(new Map(), {
get(map, name) {
if (!map.has(name)) {
console.log("creating attribute", name);
map.set(name, name);
}
return map.get(name);
},
});

mp.runPython(`
import js
print(js.obj1.a, js.obj1.b)
print(js.obj1["a"], js.obj1["b"])
print(js.obj2.undefined, js.obj2.undef)
print(js.obj3.c)
print(js.obj3["c"])
print(hasattr(js.obj3, "d"))
print(js.obj3.d)
`);
9 changes: 9 additions & 0 deletions tests/ports/webassembly/js_proxy_attribute.mjs.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
1 2
1 2
undefined <undefined>
creating attribute c
c
c
creating attribute d
True
d

0 comments on commit 95c19e0

Please sign in to comment.