diff --git a/docs/project/changelog.md b/docs/project/changelog.md index 91d4aeeca..8bcaa9810 100644 --- a/docs/project/changelog.md +++ b/docs/project/changelog.md @@ -53,6 +53,11 @@ myst: `{env: {HOME: whatever_directory}}`. {pr}`3870` +- {{ Fix }} `getattr(jsproxy, 'python_reserved_word')` works as expected again + (as well as `hasattr` and `setattr`). This fixes a regression introduced in + {pr}`3617`. + {pr}`3926` + ### Packages - OpenBLAS has been added and scipy now uses OpenBLAS rather than CLAPACK diff --git a/docs/usage/faq.md b/docs/usage/faq.md index 41f48f555..3e7d52d4e 100644 --- a/docs/usage/faq.md +++ b/docs/usage/faq.md @@ -441,30 +441,70 @@ lambda = (x) => {return x + 1}; pyodide.runPython(`from js import lambda; print(lambda(1))`); ``` -For JS objects with attributes that are Python reserved keywords, {py:func}`getattr` and {py:func}`setattr` can be used to access the attribute by name: +If you try to access a Python reserved word followed by one or more underscores +on a `JsProxy`, Pyodide will remove a single underscore: + +```pyodide +pyodide.runPython(` + from js import Array + print(Array.from_([1,2,3])) +`); +``` + +If you meant to access the keyword with an underscore at the end, you'll have to +add an extra one: + +```pyodide +globalThis.lambda = 7; +globalThis.lambda_ = 8; +pyodide.runPython(` + from js import lambda_, lambda__ + print(lambda_, lambda__) # 7, 8 +`); +``` + +Another example: + +```pyodide +people = {global: "lots and lots"}; +pyodide.runPython(` + from js import people + # the dir contains global_ but not global: + assert "global_" in dir(people) + assert "global" not in dir(people) + people.global_ = 'even more' + print(people.global_) +`); +``` + +You can also use `getattr`, `setattr`, and `delattr` to access the attribute: ```pyodide pyodide.runPython(` from js import Array fromFunc = getattr(Array, 'from') print(fromFunc([1,2,3])) - `); +`); people = {global: "lots and lots"}; pyodide.runPython(` from js import people setattr(people, 'global', 'even more') print(getattr(people, 'global')) - `); +`); ``` -For objects whose names are keywords, one can similarly use {py:func}`getattr` on the `js` module itself: +For JavaScript globals whose names are keywords, one can similarly use +{py:func}`getattr` on the `js` module itself: ```pyodide -lambda = (x) => {return x + 1}; +globalThis.lambda = 7; +globalThis.lambda_ = 8; pyodide.runPython(` import js js_lambda = getattr(js, 'lambda') - print(js_lambda(1)) - `); + js_lambda_ = getattr(js, 'lambda_') + js_lambda__ = getattr(js, 'lambda__') + print(js_lambda, js_lambda_, js_lambda__) # 7, 7, 8 +`); ``` diff --git a/src/core/hiwire.c b/src/core/hiwire.c index bf89b293c..16f8a8d45 100644 --- a/src/core/hiwire.c +++ b/src/core/hiwire.c @@ -947,7 +947,7 @@ EM_JS(bool, isReservedWord, (int word), { * action: a javascript string, one of get, set, or delete. For error reporting. * word: a javascript string, the property being accessed */ -EM_JS(int, normalizeReservedWords, (int action, int word), { +EM_JS(int, normalizeReservedWords, (int word), { // clang-format off // 1. if word is not a reserved word followed by 0 or more underscores, return // it unchanged. @@ -960,19 +960,14 @@ EM_JS(int, normalizeReservedWords, (int action, int word), { if (noTrailing_ !== word) { return word.slice(0, -1); } - // 3. If the word is exactly a reserved word, this is an error. - let action_ptr = stringToNewUTF8(action); - let word_ptr = stringToNewUTF8(word); - _setReservedError(action_ptr, word_ptr); - _free(action_ptr); - _free(word_ptr); - throw new Module._PropagatePythonError(); + // 3. If the word is exactly a reserved word, return it unchanged + return word; // clang-format on }); EM_JS_REF(JsRef, JsObject_GetString, (JsRef idobj, const char* ptrkey), { let jsobj = Hiwire.get_value(idobj); - let jskey = normalizeReservedWords("get", UTF8ToString(ptrkey)); + let jskey = normalizeReservedWords(UTF8ToString(ptrkey)); if (jskey in jsobj) { return Hiwire.new_value(jsobj[jskey]); } @@ -985,7 +980,7 @@ JsObject_SetString, (JsRef idobj, const char* ptrkey, JsRef idval), { let jsobj = Hiwire.get_value(idobj); - let jskey = normalizeReservedWords("set", UTF8ToString(ptrkey)); + let jskey = normalizeReservedWords(UTF8ToString(ptrkey)); let jsval = Hiwire.get_value(idval); jsobj[jskey] = jsval; }); @@ -993,7 +988,7 @@ JsObject_SetString, EM_JS_NUM(errcode, JsObject_DeleteString, (JsRef idobj, const char* ptrkey), { let jsobj = Hiwire.get_value(idobj); - let jskey = normalizeReservedWords("delete", UTF8ToString(ptrkey)); + let jskey = normalizeReservedWords(UTF8ToString(ptrkey)); delete jsobj[jskey]; }); diff --git a/src/tests/test_jsproxy.py b/src/tests/test_jsproxy.py index 4193f1649..478634865 100644 --- a/src/tests/test_jsproxy.py +++ b/src/tests/test_jsproxy.py @@ -2409,12 +2409,15 @@ def test_python_reserved_keywords(selenium): assert o.async___ == 3 assert getattr(o, "async_") == 1 # noqa: B009 assert getattr(o, "async__") == 2 # noqa: B009 - with pytest.raises(AttributeError, match="async"): - getattr(o, "async") - with pytest.raises(AttributeError, match="reserved.*set.*'async_'"): - setattr(o, "async", 2) - with pytest.raises(AttributeError, match="reserved.*delete.*'async_'"): - delattr(o, "async") + assert getattr(o, "async") == 1 + + assert hasattr(o, "async_") + assert hasattr(o, "async") + setattr(o, "async", 2) + assert o.async_ == 2 + delattr(o, "async") + assert not hasattr(o, "async_") + assert not hasattr(o, "async") @run_in_pyodide