Repository: pyodide/pyodide
Pull request: Add noop "bind" function to PyProxyCallableMethods
<!-- Thank you for contributing to Pyodide! All improvements are welcome,
     so don't be afraid to make a PR. -->

### Description

<!-- Please explain what your PR is about:
     - reasoning for the change
     - some details of updated code
     - any noteworthy choices to be aware of
	Please refer to any related issues by #<issue_id> -->

This pull request adds a noop "bind" function to `PyProxyCallableMethods` in pyproxy.ts. Some libraries like Vue will call "bind" on functions passed as event handlers. Adding a no-op bind will not change the functionality of the proxy, but will prevent this error.

Fixes #2757

### Checklists

<!-- Note:
     If you think some of these steps are not necessary for your PR,
     remove those checkboxes, or mark them as checked. If you keep unchecked checkboxes,
     we will assume that your PR is not ready to be merged  -->

- [ ] Add a [CHANGELOG](https://github.com/pyodide/pyodide/blob/main/docs/project/changelog.md) entry
- [ ] Add / update tests
- [ ] Add new / update outdated documentation


Problem statement:
Add "bind" to function PyProxy
## 🚀 Feature

<!-- A clear and concise description of the feature proposal -->

Some libraries like Vue are calling "bind" on functions, to attempt to bind a javascript `this` to the function. Currently, there is no corresponding function in the PyProxy of functions and it is raising an error.

### Motivation

<!-- Please outline the motivation for the proposal. Is your feature request related to a problem? e.g., I'm always frustrated when [...]. If this is related to another GitHub issue, please link here too -->

Not having `bind` is causing incompatibility with existing libraries.

### Pitch

<!-- A clear and concise description of what you want to happen. -->

Add a `bind` stub, even if it doesn't have any functionality.

### Alternatives

<!-- A clear and concise description of any alternative solutions or features you've considered, if any. -->

### Additional context

<!-- Add any other context or screenshots about the feature request here. -->

Hints from issue discussion:
Sounds reasonable to me. It would be possible to hook `bind(x)` to `f.__get__(x)` but it is not clear whether that makes any sense.
I don't know enough about the internals of PyProxy to suggest a solution.
I think that having `bind` be a no-op makes the most sense. In JavaScript if you have a function:
```js
function doesnt_expect_this(x, y){
    // Does stuff but doesn't use this
}
```
then doing `doesnt_expect_this.bind(something)(x, y)` works fine. On the other hand in Python:
```py
def f(x, y):
   # no self argument

f.__get__(None)(1,2) # TypeError: f() takes 2 positional arguments but 3 were given
```


It looks like the standard function prototype is cleared here:

https://github.com/pyodide/pyodide/blob/be331255f7dff3011bc8d7127f8ebd0e4db36524/src/core/pyproxy.ts#L149

That is deleting bind and other features.
Maybe we should just remove that then. We definitely want to get rid of `name` and `length` because otherwise those would cover up common Python attributes. Though I guess it's not so common for a Python callable to have either of these properties. In any case, a Python callable with a `prototype` attribute is probably not common enough to worry about.
I think the expected behavior is that a Python function will behave the same as a JS function using the proxy. Previous versions worked this way. The change of behavior is breaking some of my existing code.
That line was added in v0.17.0 in this PR:
https://github.com/pyodide/pyodide/pull/1264

Commit message: Add compare bind return to test_pyproxy_call

Patch to review:
diff --git a/src/core/pyproxy.ts b/src/core/pyproxy.ts
index 1c304a390c8..ddfc6603918 100644
--- a/src/core/pyproxy.ts
+++ b/src/core/pyproxy.ts
@@ -1140,6 +1140,13 @@ export class PyProxyCallableMethods {
     }
     return Module.callPyObjectKwargs(_getPtr(this), ...jsargs);
   }
+
+  /**
+   * No-op bind function for compatibility with existing libraries
+   */
+  bind(placeholder: any) {
+    return this;
+  }
 }
 // @ts-ignore
 PyProxyCallableMethods.prototype.prototype = Function.prototype;
diff --git a/src/tests/test_pyproxy.py b/src/tests/test_pyproxy.py
index 53ea2010135..da24bbe5ed2 100644
--- a/src/tests/test_pyproxy.py
+++ b/src/tests/test_pyproxy.py
@@ -888,6 +888,9 @@ def assert_call(s, val):
     with pytest.raises(selenium.JavascriptException, match=msg):
         selenium.run_js("f.callKwargs(76, {x : 6})")
 
+    assert_call("f.bind({})()", [2, 3])
+    assert_call("f.bind({}).$$ == f.$$", True)
+
     selenium.run_js("f.destroy()")
 
 


Code review comments:
diff_hunk: @@ -888,6 +888,9 @@ def assert_call(s, val):
     with pytest.raises(selenium.JavascriptException, match=msg):
         selenium.run_js("f.callKwargs(76, {x : 6})")
 
+    assert_call("f.bind({})()", [2, 3])
+    assert_call("f.bind({}).$$ == f.$$", True)
path: src/tests/test_pyproxy.py
text: @user1:
What about:
```suggestion
    assert_call("f.bind({}) === f", True)
```

@author:
Directly comparing functions didn't seem to work.

@user1:
Okay. Let's use `===` though.

@author:
Done