Don't throw when calling str on a proxy without a toString method (#4574)

Resolves #4569. It still doesn't make sure `str(jsproxy)` never throws. It will throw if:
1. accessing `obj.toString` succeeds and returns a function, but calling the function throws
2. accessing `obj.toString` fails or returns not a function, and `Object.prototype.toString.call` fails.
This commit is contained in:
Hood Chatham 2024-03-25 13:15:25 +01:00 committed by GitHub
parent 9d5870478c
commit c9809eea86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 79 additions and 8 deletions

View File

@ -16,6 +16,13 @@ myst:
## Unreleased
- {{ Enhancement }} `str(jsproxy)` has been adjusted to not raise an error if
`jsproxy.toString` is undefined. Instead, it will use
`Object.prototype.toString` in this case. If `jsproxy.toString` is defined and
throws or is not defined but `jsproxy[Symbol.toStringTag]` is defined and
throws, then `str` will still raise.
{pr}`4574`
- {{ Enhancement }} Improved support for stack switching.
{pr}`4532`, {pr}`4547`

View File

@ -279,7 +279,10 @@ EM_JS_VAL(JsVal, JsvObject_Values, (JsVal obj), {
EM_JS_VAL(JsVal,
JsvObject_toString, (JsVal obj), {
return obj.toString();
if (hasMethod(obj, "toString")) {
return obj.toString();
}
return Object.prototype.toString.call(obj);
});

View File

@ -269,9 +269,6 @@ JsProxy_Repr(PyObject* self)
{
JsVal repr = JsvObject_toString(JsProxy_VAL(self));
if (JsvNull_Check(repr)) {
PyErr_Format(PyExc_TypeError,
"Pyodide cannot generate a repr for this Javascript object "
"because it has no 'toString' method");
return NULL;
}
return js2python(repr);

View File

@ -1286,13 +1286,10 @@ def test_js_id(selenium):
@run_in_pyodide
def test_object_with_null_constructor(selenium):
from unittest import TestCase
from pyodide.code import run_js
o = run_js("Object.create(null)")
with TestCase().assertRaises(TypeError):
repr(o)
assert repr(o) == "[object Object]"
@pytest.mark.parametrize("n", [1 << 31, 1 << 32, 1 << 33, 1 << 63, 1 << 64, 1 << 65])
@ -2557,3 +2554,70 @@ def test_js_proxy_attribute(selenium):
assert x.c is None
with pytest.raises(AttributeError):
x.d # noqa: B018
@run_in_pyodide
async def test_js_proxy_str(selenium):
import re
import pytest
from js import Array
from pyodide.code import run_js
from pyodide.ffi import JsException
assert (
re.sub(r"\s+", " ", str(Array).replace("\n", " "))
== "function Array() { [native code] }"
)
assert str(run_js("[1,2,3]")) == "1,2,3"
assert str(run_js("Object.create(null)")) == "[object Object]"
mod = await run_js("import('data:text/javascript,')")
assert str(mod) == "[object Module]"
# accessing toString fails, should fall back to Object.prototype.toString.call
x = run_js(
"""
({
get toString() {
throw new Error();
},
[Symbol.toStringTag] : "SomeTag"
})
"""
)
assert str(x) == "[object SomeTag]"
# accessing toString succeeds but toString call throws, let exception propagate
x = run_js(
"""
({
toString() {
throw new Error("hi!");
},
})
"""
)
with pytest.raises(JsException, match="hi!"):
str(x)
# No toString method, so we fall back to Object.prototype.toString.call
# which throws, let error propagate
x = run_js(
"""
({
get [Symbol.toStringTag]() {
throw new Error("hi!");
},
});
"""
)
with pytest.raises(JsException, match="hi!"):
str(x)
# accessing toString fails, so fall back to Object.prototype.toString.call
# which also throws, let error propagate
px = run_js("(p = Proxy.revocable({}, {})); p.revoke(); p.proxy")
with pytest.raises(
JsException,
match="revoked",
):
str(px)