mirror of https://github.com/pyodide/pyodide.git
Handling for reserved Python keywords as JsProxy attrs (#3617)
Resolves #3615. Things like `from` and `finally` are reserved keywords in Python and so `a.finally` is a `SyntaxError`. This automatically reroutes `a.from_` on a `JsProxy` to refer to `a.from` so it can be used reasonably conveniently from Python.
This commit is contained in:
parent
ce7880739e
commit
97d17373f2
|
@ -15,6 +15,15 @@ myst:
|
|||
|
||||
## Unreleased
|
||||
|
||||
- {{ Enhancement }} Python does not allow reserved words to be used as attributes.
|
||||
For instance, `Array.from` is a `SyntaxError`. (JavaScript has a more robust
|
||||
parser which can handle this.) To handle this, if an attribute to a `JsProxy`
|
||||
consists of a Python reserved word followed by one or more underscores, we remove
|
||||
a single underscore from the end of the attribute. For instance, `Array.from_`
|
||||
would access `from` on the underlying JavaScript object, whereas `o.from__`
|
||||
accesses the `from_` attribute.
|
||||
{pr}`3617`
|
||||
|
||||
- {{ Enhancement }} `runPython` and `runPythonAsync` now accept a `locals`
|
||||
argument.
|
||||
{pr}`3618`
|
||||
|
|
|
@ -208,6 +208,20 @@ obj_map = obj.as_object_map()
|
|||
assert obj_map["$c"] == 11
|
||||
```
|
||||
|
||||
Another special case comes from the fact that Python reserved words cannot be
|
||||
used as attributes. For instance, {js:func}`Array.from` and
|
||||
{js:meth}`Promise.finally` cannot be directly accessed because they are Python
|
||||
`SyntaxError`s. Instead we access these attributes with `Array.from_` and
|
||||
`Promise.finally_`. Similarly, to access from Python, `o.from_` you have to use
|
||||
`o.from__` with two underscores (since a single underscore is used for
|
||||
`o.from`). This is reflected in the `dir` of a `JsProxy`:
|
||||
|
||||
```py
|
||||
from pyodide.code import run_js
|
||||
o = run_js("({finally: 1, return: 2, from: 3, from_: 4})")
|
||||
assert set(dir(o)) == {"finally_", "return_", "from_", "from__"}
|
||||
```
|
||||
|
||||
(type-translations-pyproxy)=
|
||||
|
||||
### Proxying from Python into JavaScript
|
||||
|
|
|
@ -307,7 +307,6 @@ API.PythonError = PythonError;
|
|||
// appropriate error value (either NULL or -1).
|
||||
class _PropagatePythonError extends Error {
|
||||
constructor() {
|
||||
API.fail_test = true;
|
||||
super(
|
||||
"If you are seeing this message, an internal Pyodide error has " +
|
||||
"occurred. Please report it to the Pyodide maintainers.",
|
||||
|
|
|
@ -908,25 +908,74 @@ EM_JS_REF(JsRef, JsObject_New, (), {
|
|||
});
|
||||
// clang-format on
|
||||
|
||||
void
|
||||
setReservedError(char* action, char* word)
|
||||
{
|
||||
PyErr_Format(PyExc_AttributeError,
|
||||
"The string '%s' is a Python reserved word. To %s an attribute "
|
||||
"on a JS object called '%s' use '%s_'.",
|
||||
word,
|
||||
action,
|
||||
word,
|
||||
word);
|
||||
}
|
||||
|
||||
EM_JS(bool, isReservedWord, (int word), {
|
||||
if (!Module.pythonReservedWords) {
|
||||
Module.pythonReservedWords = new Set([
|
||||
"False", "await", "else", "import", "pass", "None", "break",
|
||||
"except", "in", "raise", "True", "class", "finally", "is",
|
||||
"return", "and", "continue", "for", "lambda", "try", "as",
|
||||
"def", "from", "nonlocal", "while", "assert", "del", "global",
|
||||
"not", "with", "async", "elif", "if", "or", "yield",
|
||||
])
|
||||
}
|
||||
return Module.pythonReservedWords.has(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), {
|
||||
// clang-format off
|
||||
// 1. if word is not a reserved word followed by 0 or more underscores, return
|
||||
// it unchanged.
|
||||
const noTrailing_ = word.replace(/_*$/, "");
|
||||
if (!isReservedWord(noTrailing_)) {
|
||||
return word;
|
||||
}
|
||||
// 2. If there is at least one trailing underscore, return the word with a
|
||||
// single underscore removed.
|
||||
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();
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
EM_JS_REF(JsRef, JsObject_GetString, (JsRef idobj, const char* ptrkey), {
|
||||
let jsobj = Hiwire.get_value(idobj);
|
||||
let jskey = UTF8ToString(ptrkey);
|
||||
let result = jsobj[jskey];
|
||||
// clang-format off
|
||||
if (result === undefined && !(jskey in jsobj)) {
|
||||
// clang-format on
|
||||
return ERROR_REF;
|
||||
let jskey = normalizeReservedWords("get", UTF8ToString(ptrkey));
|
||||
if (jskey in jsobj) {
|
||||
return Hiwire.new_value(jsobj[jskey]);
|
||||
}
|
||||
return Hiwire.new_value(result);
|
||||
return ERROR_REF;
|
||||
});
|
||||
|
||||
// clang-format off
|
||||
EM_JS_NUM(errcode,
|
||||
JsObject_SetString,
|
||||
(JsRef idobj, const char* ptrkey, JsRef idval),
|
||||
JsObject_SetString,
|
||||
(JsRef idobj, const char* ptrkey, JsRef idval),
|
||||
{
|
||||
let jsobj = Hiwire.get_value(idobj);
|
||||
let jskey = UTF8ToString(ptrkey);
|
||||
let jskey = normalizeReservedWords("set", UTF8ToString(ptrkey));
|
||||
let jsval = Hiwire.get_value(idval);
|
||||
jsobj[jskey] = jsval;
|
||||
});
|
||||
|
@ -934,7 +983,7 @@ EM_JS_NUM(errcode,
|
|||
|
||||
EM_JS_NUM(errcode, JsObject_DeleteString, (JsRef idobj, const char* ptrkey), {
|
||||
let jsobj = Hiwire.get_value(idobj);
|
||||
let jskey = UTF8ToString(ptrkey);
|
||||
let jskey = normalizeReservedWords("delete", UTF8ToString(ptrkey));
|
||||
delete jsobj[jskey];
|
||||
});
|
||||
|
||||
|
@ -943,12 +992,16 @@ EM_JS_REF(JsRef, JsObject_Dir, (JsRef idobj), {
|
|||
let result = [];
|
||||
do {
|
||||
// clang-format off
|
||||
result.push(... Object.getOwnPropertyNames(jsobj).filter(
|
||||
const names = Object.getOwnPropertyNames(jsobj);
|
||||
result.push(...names.filter(
|
||||
s => {
|
||||
let c = s.charCodeAt(0);
|
||||
return c < 48 || c > 57; /* Filter out integer array indices */
|
||||
}
|
||||
));
|
||||
)
|
||||
// If the word is a reserved word followed by 0 or more underscores, add an
|
||||
// extra underscore to reverse the transformation applied by normalizeReservedWords.
|
||||
.map(word => isReservedWord(word.replace(/_*$/, "")) ? word + "_" : word));
|
||||
// clang-format on
|
||||
} while (jsobj = Object.getPrototypeOf(jsobj));
|
||||
return Hiwire.new_value(result);
|
||||
|
|
|
@ -2197,3 +2197,63 @@ async def test_agen_lifetimes(selenium):
|
|||
del res
|
||||
assert v == ["{1}", "{2}", "{3}", "{4}"]
|
||||
assert sys.getrefcount(v) == 2
|
||||
|
||||
|
||||
@run_in_pyodide
|
||||
def test_python_reserved_keywords(selenium):
|
||||
import pytest
|
||||
from pyodide.code import run_js
|
||||
|
||||
o = run_js(
|
||||
"""({
|
||||
async: 1,
|
||||
await: 2,
|
||||
False: 3,
|
||||
nonlocal: 4,
|
||||
yield: 5,
|
||||
try: 6,
|
||||
assert: 7,
|
||||
match: 222,
|
||||
})
|
||||
"""
|
||||
)
|
||||
assert o.match == 222
|
||||
with pytest.raises(AttributeError):
|
||||
o.match_
|
||||
assert eval("o.match") == 222
|
||||
keys = ["async", "await", "False", "nonlocal", "yield", "try", "assert"]
|
||||
for k in keys:
|
||||
with pytest.raises(SyntaxError):
|
||||
eval(f"o.{k}")
|
||||
|
||||
assert o.async_ == 1
|
||||
assert o.await_ == 2
|
||||
assert o.False_ == 3
|
||||
assert o.nonlocal_ == 4
|
||||
assert o.yield_ == 5
|
||||
assert o.try_ == 6
|
||||
assert o.assert_ == 7
|
||||
expected_set = {k + "_" for k in keys} | {"match"}
|
||||
actual_set = set(dir(o)) & expected_set
|
||||
assert actual_set == expected_set
|
||||
assert set(dir(o)) & set(keys) == set()
|
||||
o.async_ = 2
|
||||
assert run_js("(o) => o.async")(o) == 2
|
||||
del o.async_
|
||||
assert run_js("(o) => o.async")(o) == None
|
||||
|
||||
o = run_js("({async: 1, async_: 2, async__: 3})")
|
||||
expected_set = {"async_", "async__", "async___"}
|
||||
actual_set = set(dir(o)) & expected_set
|
||||
assert actual_set == expected_set
|
||||
assert o.async_ == 1
|
||||
assert o.async__ == 2
|
||||
assert o.async___ == 3
|
||||
assert getattr(o, "async_") == 1
|
||||
assert getattr(o, "async__") == 2
|
||||
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")
|
||||
|
|
Loading…
Reference in New Issue