Repository: pyodide/pyodide

Issue:
Nested attribute access in JS->Python type conversion
Currently the following code fails,
```js
>>> from js import window
>>> window.URL.createObjectURL
Error: Traceback (most recent call last):
  File "/lib/python3.7/site-packages/pyodide.py", line 45, in eval_code
    return eval(compile(expr, '<eval>', mode='eval'), ns, ns)
  File "<eval>", line 1, in <module>
AttributeError: 'JsBoundMethod' object has no attribute 'createObjectURL'
```
(while `window.URL.createObjectURL` is a valid JS object) because nested attributes (i.e. attribute of an attribute) don't seem to be supported. It would have been nice to make it work, though I have not looked at how difficult that would be.
from js import fetch treats fetch as a free function
`fetch` is a member function of `window`.
However, using `from js import fetch` doesn't realize that and leads to the error:

`TypeError: 'fetch' called on an object that does not implement interface Window.`

For Reproducing the Error:
```
%%py

from js import document, Request, fetch, URL
img_tag = document.createElement('img')
req = Request.new('https://i.ibb.co/3f4yJQS/face4.jpg')

def func(response):
    return response.blob()

def func2(blob):
    objURL = URL.createObjectURL(blob)
    img_tag.src = objURL

fetch(req).then(func).then(func2)

document.body.appendChild(img_tag)
```


Discussion:
Ah this is another issue with the typeconversions code! I'll try to fix it.
The issue here is a bad interaction between `pyproxy.c` and `jsproxy.c`. In particular, if you index a `jsproxy` and the value of the field  is a `pyproxy` then instead of returning the wrapped python object `jsproxy` returns a `JsBoundMethod`. It does this because the `pyproxy` is a function. Why is it a function? Well it might implement `__call__` and javascript throws an error if you try to call something that is not a function. So the `PyProxy` is a function.
https://github.com/iodide-project/pyodide/blob/41592637e6a5231d1fa74235ec5f46919c18e25d/src/type_conversion/pyproxy.c#L144

Then `JsProxy` should check first whether the function is a `PyProxy` or not before just blindly returning a `JsBoundMethod`.

https://github.com/iodide-project/pyodide/blob/master/src/type_conversion/jsproxy.c#L70

I can submit a PR to fix this, though this is code that I plan to rewrite much more drastically soon.


Resolving patch:
diff --git a/src/pyodide-py/pyodide/_core.py b/src/pyodide-py/pyodide/_core.py
--- a/src/pyodide-py/pyodide/_core.py
+++ b/src/pyodide-py/pyodide/_core.py
@@ -2,7 +2,7 @@
 import platform
 
 if platform.system() == "Emscripten":
-    from _pyodide_core import JsProxy, JsBoundMethod, JsException
+    from _pyodide_core import JsProxy, JsMethod, JsException
 else:
     # Can add shims here if we are so inclined.
     class JsException(Exception):
@@ -17,10 +17,10 @@ class JsProxy:
 
         # Defined in jsproxy.c
 
-    class JsBoundMethod:
+    class JsMethod:
         """A proxy to make it possible to call Javascript bound methods from Python."""
 
         # Defined in jsproxy.c
 
 
-__all__ = [JsProxy, JsBoundMethod, JsException]
+__all__ = [JsProxy, JsMethod, JsException]


Test patch:
diff --git a/src/tests/test_jsproxy.py b/src/tests/test_jsproxy.py
--- a/src/tests/test_jsproxy.py
+++ b/src/tests/test_jsproxy.py
@@ -7,29 +7,38 @@ def test_jsproxy_dir(selenium):
     result = selenium.run_js(
         """
         window.a = { x : 2, y : "9" };
+        window.b = function(){};
         return pyodide.runPython(`
             from js import a
-            dir(a)
+            from js import b
+            [dir(a), dir(b)]
         `);
         """
     )
-    assert set(result).issuperset(
+    jsproxy_items = set(
         [
             "__bool__",
-            "__call__",
             "__class__",
             "__defineGetter__",
             "__defineSetter__",
             "__delattr__",
             "__delitem__",
             "constructor",
-            "new",
             "toString",
             "typeof",
             "valueOf",
-            "x",
         ]
     )
+    a_items = set(["x", "y"])
+    callable_items = set(["__call__", "new"])
+    set0 = set(result[0])
+    set1 = set(result[1])
+    assert set0.issuperset(jsproxy_items)
+    assert set0.isdisjoint(callable_items)
+    assert set0.issuperset(a_items)
+    assert set1.issuperset(jsproxy_items)
+    assert set1.issuperset(callable_items)
+    assert set1.isdisjoint(a_items)
 
 
 def test_jsproxy_getattr(selenium):
@@ -245,7 +254,6 @@ def test_jsproxy_call_meth_js(selenium):
     )
 
 
-@pytest.mark.xfail
 def test_jsproxy_call_meth_js_kwargs(selenium):
     assert selenium.run_js(
         """
@@ -257,7 +265,8 @@ def test_jsproxy_call_meth_js_kwargs(selenium):
         return pyodide.runPython(
             `
             from js import a
-            a.f(y=10, x=2) == [a, x, y]
+            [r0, r1, r2] = a.f(y=10, x=2)
+            r0 == a and r1 == 2 and r2 == 10
             `
         );
         """