Repository: pyodide/pyodide

Issue:
Proxied JS method.apply(context, list) fail while method(*list) doesn't
## 🐛 Bug

While working on [this PyScript issue](https://github.com/pyscript/pyscript/pull/1459) I've noticed that `context.method.apply(context, list)` doesn't work while `context.method(*list)` does.

I don't mind using the latter as that's also more Pythonic but that might surprise JS developers using Pyodide proxies that mimic JS APIs.

### To Reproduce

```python
import js
classList = js.document.body.classList
classList.add.apply(classList, ["a", "b"])
```

### Expected behavior

The method should be invoked with *n* arguments as by JS specs.

### Environment

- Pyodide Version<!-- (e.g. 1.8.1) -->: latest
- Browser version<!-- (e.g. Chrome 95.0.4638.54) -->: any
- Any other relevant information: nope

<!-- If you are building Pyodide by yourself, please also include these information: -->

<!--
- Commit hash of Pyodide git repository:
- Build environment<!--(e.g. Ubuntu 18.04, pyodide/pyodide-env:19 docker)- ->:
-->

### Additional context

Happy to have it as won't fix but at least there's a related issue that explain the *gotcha*.



Discussion:
Unraveling this a bit:
1.
```js
function a(...params) {
    console.log(params);
}
pyodide.runPython(`
from js import a
a.apply(None, [7, 6, 5])
`)
```
prints `[undefined, undefined, undefined]`.

2.
```js
function a(...params) {
    console.log(params);
}
let l = pyodide.runPython(`[7, 6, 5]`)
a.apply(null, l);
```
also prints `[undefined, undefined, undefined]`.


Per the tc39 spec, [`apply`](https://tc39.es/ecma262/#sec-function.prototype.apply) uses [`CreateListFromArrayLike`](https://tc39.es/ecma262/#sec-createlistfromarraylike) to generate the argument list. The relevant snippet of `CreateListFromArrayLike` is:

> 5. Let index be 0.
> 6. Repeat, while index < len,
> 
>     a. Let indexName be ! [ToString](https://tc39.es/ecma262/#sec-tostring)([𝔽](https://tc39.es/ecma262/#%F0%9D%94%BD)(index)).
>     b. Let next be ? [Get](https://tc39.es/ecma262/#sec-get-o-p)(obj, indexName).
>     c. If elementTypes does not contain [Type](https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values)(next), throw a TypeError exception.
>     d. Append next to list.
>     e. Set index to index + 1.


Focusing in on this step 6b, we see the problem:
```js
let l = pyodide.runPython(`[7, 6, 5]`);
console.log(l[0]); // undefined
```
Oops, `l[0]` should probably give `7`, not undefined.
Not sure if proxy related but indexes in the get (or has) are always passed as strings, even for proxied arrays 
I have a fix on a local branch, but I've gotten distracted implementing other `Array.prototype` methods on PyProxies...

Resolving patch:
diff --git a/docs/sphinx_pyodide/sphinx_pyodide/mdn_xrefs.py b/docs/sphinx_pyodide/sphinx_pyodide/mdn_xrefs.py
--- a/docs/sphinx_pyodide/sphinx_pyodide/mdn_xrefs.py
+++ b/docs/sphinx_pyodide/sphinx_pyodide/mdn_xrefs.py
@@ -55,6 +55,25 @@
         "Function.apply": "$global/",
         "Function.bind": "$global/",
         "Function.call": "$global/",
+        "Array.join": "$global/",
+        "Array.slice": "$global/",
+        "Array.lastIndexOf": "$global/",
+        "Array.indexOf": "$global/",
+        "Array.forEach": "$global/",
+        "Array.map": "$global/",
+        "Array.filter": "$global/",
+        "Array.reduce": "$global/",
+        "Array.reduceRight": "$global/",
+        "Array.some": "$global/",
+        "Array.every": "$global/",
+        "Array.at": "$global/",
+        "Array.concat": "$global/",
+        "Array.includes": "$global/",
+        "Array.entries": "$global/",
+        "Array.keys": "$global/",
+        "Array.values": "$global/",
+        "Array.find": "$global/",
+        "Array.findIndex": "$global/",
     },
     "js:data": {
         "Iterable": "$reference/Iteration_protocols#the_iterable_protocol",


Test patch:
diff --git a/src/tests/test_pyproxy.py b/src/tests/test_pyproxy.py
--- a/src/tests/test_pyproxy.py
+++ b/src/tests/test_pyproxy.py
@@ -2,7 +2,7 @@
 import time
 
 import pytest
-from pytest_pyodide import run_in_pyodide
+from pytest_pyodide.decorator import run_in_pyodide
 
 
 def test_pyproxy_class(selenium):
@@ -1578,3 +1578,521 @@ async def test_multiple_interpreters(selenium):
     d1 = {"a": 2}
     d2 = py2.runPython(str(d1))
     assert d2.toJs().to_py() == d1
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_index(selenium):
+    from pyodide.code import run_js
+
+    pylist = [9, 8, 7]
+    jslist = run_js(
+        """
+        (p) => {
+            return [p[0], p[1], p[2]]
+        }
+        """
+    )(pylist)
+    assert jslist.to_py() == pylist
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_join(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = ["Wind", "Water", "Fire"]
+    ajs = to_js(a)
+    func = run_js("((a, k) => a.join(k))")
+
+    assert func(a, None) == func(ajs, None)
+    assert func(a, ", ") == func(ajs, ", ")
+    assert func(a, " ") == func(ajs, " ")
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_slice(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = ["ant", "bison", "camel", "duck", "elephant"]
+    ajs = to_js(a)
+
+    func_strs = [
+        "a.slice(2)",
+        "a.slice(2, 4)",
+        "a.slice(1, 5)",
+        "a.slice(-2)",
+        "a.slice(2, -1)",
+        "a.slice()",
+    ]
+    for func_str in func_strs:
+        func = run_js(f"(a) => {func_str}")
+        assert func(a).to_py() == func(ajs).to_py()
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_indexOf(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = ["ant", "bison", "camel", "duck", "bison"]
+    ajs = to_js(a)
+
+    func_strs = [
+        "beasts.indexOf('bison')",
+        "beasts.indexOf('bison', 2)",
+        "beasts.indexOf('bison', -4)",
+        "beasts.indexOf('bison', 3)",
+        "beasts.indexOf('giraffe')",
+    ]
+    for func_str in func_strs:
+        func = run_js(f"(beasts) => {func_str}")
+        assert func(a) == func(ajs)
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_lastIndexOf(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = ["ant", "bison", "camel", "duck", "bison"]
+    ajs = to_js(a)
+
+    func_strs = [
+        "beasts.lastIndexOf('bison')",
+        "beasts.lastIndexOf('bison', 2)",
+        "beasts.lastIndexOf('bison', -4)",
+        "beasts.lastIndexOf('bison', 3)",
+        "beasts.lastIndexOf('giraffe')",
+    ]
+    for func_str in func_strs:
+        func = run_js(f"(beasts) => {func_str}")
+        assert func(a) == func(ajs)
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_forEach(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = ["a", "b", "c"]
+    ajs = to_js(a)
+
+    func = run_js(
+        """
+        ((a) => {
+            let s = "";
+            a.forEach((elt, idx, list) => {
+                s += "::";
+                s += idx;
+                s += elt;
+                s += this[elt];
+            },
+                {a: 6, b: 9, c: 22}
+            );
+            return s;
+        })
+        """
+    )
+
+    assert func(a) == func(ajs)
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_map(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = ["a", "b", "c"]
+    ajs = to_js(a)
+    func = run_js(
+        """
+        (a) => a.map(
+            function (elt, idx, list){
+                return [elt, idx, this[elt]]
+            },
+            {a: 6, b: 9, c: 22}
+        )
+        """
+    )
+    assert func(a).to_py() == func(ajs).to_py()
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_filter(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = list(range(20, 0, -2))
+    ajs = to_js(a)
+    func = run_js(
+        """
+        (a) => a.filter(
+            function (elt, idx){
+                return elt + idx > 12
+            }
+        )
+        """
+    )
+    assert func(a).to_py() == func(ajs).to_py()
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_reduce(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = list(range(20, 0, -2))
+    ajs = to_js(a)
+    func = run_js(
+        """
+        (a) => a.reduce((l, r) => l + 2*r)
+        """
+    )
+    assert func(a) == func(ajs)
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_reduceRight(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = list(range(20, 0, -2))
+    ajs = to_js(a)
+    func = run_js(
+        """
+        (a) => a.reduceRight((l, r) => l + 2*r)
+        """
+    )
+    assert func(a) == func(ajs)
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_some(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    func = run_js("(a) => a.some((element, idx) => (element + idx) % 2 === 0)")
+    for a in [
+        [1, 2, 3, 4, 5],
+        [2, 3, 4, 5],
+        [1, 3, 5],
+        [1, 4, 5],
+        [4, 5],
+    ]:
+        assert func(a) == func(to_js(a))
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_every(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    func = run_js("(a) => a.every((element, idx) => (element + idx) % 2 === 0)")
+    for a in [
+        [1, 2, 3, 4, 5],
+        [2, 3, 4, 5],
+        [1, 3, 5],
+        [1, 4, 5],
+        [4, 5],
+    ]:
+        assert func(a) == func(to_js(a))
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_at(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = [5, 12, 8, 130, 44]
+    ajs = to_js(a)
+
+    func = run_js("(a, idx) => a.at(idx)")
+    for idx in [2, 3, 4, -2, -3, -4, 5, 7, -7]:
+        assert func(a, idx) == func(ajs, idx)
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_concat(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = [[5, 12, 8], [130, 44], [6, 7, 7]]
+    ajs = to_js(a)
+
+    func = run_js("(a, b, c) => a.concat(b, c)")
+    assert func(*a).to_py() == func(*ajs).to_py()
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_includes(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = [5, 12, 8, 130, 44, 6, 7, 7]
+    ajs = to_js(a)
+
+    func = run_js("(a, n) => a.includes(n)")
+    for n in range(4, 10):
+        assert func(a, n) == func(ajs, n)
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_entries(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = [5, 12, 8, 130, 44, 6, 7, 7]
+    ajs = to_js(a)
+
+    func = run_js("(a, k) => Array.from(a[k]())")
+    for k in ["entries", "keys", "values"]:
+        assert func(a, k).to_py() == func(ajs, k).to_py()
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_find(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = [5, 12, 8, 130, 44, 6, 7, 7]
+    ajs = to_js(a)
+
+    func = run_js("(a, k) => a[k](element => element > 10)")
+    for k in ["find", "findIndex"]:
+        assert func(a, k) == func(ajs, k)
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_sort(selenium):
+    # from
+    # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#creating_displaying_and_sorting_an_array
+    # Yes, JavaScript sort is weird.
+    from pyodide.code import run_js
+
+    stringArray = ["Blue", "Humpback", "Beluga"]
+    numberArray = [40, None, 1, 5, 200]
+    numericStringArray = ["80", "9", "700"]
+    mixedNumericArray = ["80", "9", "700", 40, 1, 5, 200]
+
+    run_js("globalThis.compareNumbers = (a, b) => a - b")
+
+    assert run_js("((a) => a.join())")(stringArray) == "Blue,Humpback,Beluga"
+    assert run_js("((a) => a.sort())")(stringArray) is stringArray
+    assert stringArray == ["Beluga", "Blue", "Humpback"]
+
+    assert run_js("((a) => a.join())")(numberArray) == "40,,1,5,200"
+    assert run_js("((a) => a.sort())")(numberArray) == [1, 200, 40, 5, None]
+    assert run_js("((a) => a.sort(compareNumbers))")(numberArray) == [
+        1,
+        5,
+        40,
+        200,
+        None,
+    ]
+
+    assert run_js("((a) => a.join())")(numericStringArray) == "80,9,700"
+    assert run_js("((a) => a.sort())")(numericStringArray) == ["700", "80", "9"]
+    assert run_js("((a) => a.sort(compareNumbers))")(numericStringArray) == [
+        "9",
+        "80",
+        "700",
+    ]
+
+    assert run_js("((a) => a.join())")(mixedNumericArray) == "80,9,700,40,1,5,200"
+    assert run_js("((a) => a.sort())")(mixedNumericArray) == [
+        1,
+        200,
+        40,
+        5,
+        "700",
+        "80",
+        "9",
+    ]
+    assert run_js("((a) => a.sort(compareNumbers))")(mixedNumericArray) == [
+        1,
+        5,
+        "9",
+        40,
+        "80",
+        200,
+        "700",
+    ]
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_reverse(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = [3, 2, 4, 1, 5]
+    ajs = to_js(a)
+
+    func = run_js("((a) => a.reverse())")
+    assert func(a) is a
+    func(ajs)
+    assert ajs.to_py() == a
+
+
+@pytest.mark.parametrize(
+    "func",
+    [
+        'splice(2, 0, "drum")',
+        'splice(2, 0, "drum", "guitar")',
+        "splice(3, 1)",
+        'splice(2, 1, "trumpet")',
+        'splice(0, 2, "parrot", "anemone", "blue")',
+        "splice(2, 2)",
+        "splice(-2, 1)",
+        "splice(2)",
+        "splice()",
+    ],
+)
+@run_in_pyodide
+def test_pyproxy_of_list_splice(selenium, func):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = ["angel", "clown", "mandarin", "sturgeon"]
+    ajs = to_js(a)
+
+    func = run_js(f"((a) => a.{func})")
+    assert func(a).to_py() == func(ajs).to_py()
+    assert a == ajs.to_py()
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_push(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = [4, 5, 6]
+    ajs = to_js(a)
+
+    func = run_js("(a) => a.push(1, 2, 3)")
+    assert func(a) == func(ajs)
+    assert ajs.to_py() == a
+
+    a = [4, 5, 6]
+    ajs = to_js(a)
+    func = run_js(
+        """
+        (a) => {
+            a.push(1);
+            a.push(2);
+            return a.push(3);
+        }
+        """
+    )
+    assert func(a) == func(ajs)
+    assert ajs.to_py() == a
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_pop(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    func = run_js("((a) => a.pop())")
+
+    for a in [
+        [],
+        ["broccoli", "cauliflower", "cabbage", "kale", "tomato"],
+    ]:
+        ajs = to_js(a)
+        assert func(a) == func(ajs)
+        assert ajs.to_py() == a
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_shift(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = ["Andrew", "Tyrone", "Paul", "Maria", "Gayatri"]
+    ajs = to_js(a)
+
+    func = run_js(
+        """
+        (a) => {
+            let result = [];
+            while (typeof (i = a.shift()) !== "undefined") {
+                result.push(i);
+            }
+            return result;
+        }
+        """
+    )
+    assert func(a).to_py() == func(ajs).to_py()
+    assert a == []
+    assert ajs.to_py() == []
+
+
+@run_in_pyodide
+def test_pyproxy_of_list_unshift(selenium):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = [4, 5, 6]
+    ajs = to_js(a)
+
+    func = run_js("(a) => a.unshift(1, 2, 3)")
+    assert func(a) == func(ajs)
+    assert ajs.to_py() == a
+
+    a = [4, 5, 6]
+    ajs = to_js(a)
+    func = run_js(
+        """
+        (a) => {
+            a.unshift(1);
+            a.unshift(2);
+            return a.unshift(3);
+        }
+        """
+    )
+    assert func(a) == func(ajs)
+    assert ajs.to_py() == a
+
+
+@pytest.mark.parametrize(
+    "func",
+    [
+        "copyWithin(-2)",
+        "copyWithin(0, 3)",
+        "copyWithin(0, 3, 4)",
+        "copyWithin(-2, -3, -1)",
+    ],
+)
+@run_in_pyodide
+def test_pyproxy_of_list_copyWithin(selenium, func):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = ["a", "b", "c", "d", "e"]
+    ajs = to_js(a)
+    func = run_js(f"(a) => a.{func}")
+    assert func(a) is a
+    func(ajs)
+    assert a == ajs.to_py()
+
+
+@pytest.mark.parametrize(
+    "func",
+    [
+        "fill(0, 2, 4)",
+        "fill(5, 1)",
+        "fill(6)",
+    ],
+)
+@run_in_pyodide
+def test_pyproxy_of_list_fill(selenium, func):
+    from pyodide.code import run_js
+    from pyodide.ffi import to_js
+
+    a = ["a", "b", "c", "d", "e"]
+    ajs = to_js(a)
+    func = run_js(f"(a) => a.{func}")
+    assert func(a) is a
+    func(ajs)
+    assert a == ajs.to_py()