Repository: pyodide/pyodide

Issue:
Use leading $ to distinguish shadowed PyProxy methods
I am thinking again of the problem of a Python class with a method that gets shadowed by a PyProxy method. For example:
```js
let t = pyodide.runPython(`
class Test:
   def destroy(self):
      print("destroy!")
Test()
`);
t.destroy(); // should this destroy the PyProxy or call `Test.destroy`?
```
`$` is legal in Javascript identifiers but not in Python identifiers. So I think we should use a trailing `$` for one of these and no trailing `$` for the other:
```js
t.destroy$(); // prints "destroy!"
t.destroy();  // destroys proxy
```
Or maybe the reverse? Or maybe:
```js
t.destroy$py(); // prints "destroy!"
t.destroy$js();  // destroys proxy
```
not sure what the best option here is, but I think using `$` is a sound approach.


Discussion:
For completeness sake, some more options would be
- Prefixing every PyProxy method with a `$`, like `t.$destroy()` or `t.$clone()`
- Using symbols like `t[PyProxy.destroy]()` 
- Using static methods `PyProxy.destroy(t)`, which could use symbols under the hood
Thanks for opening this issue. I think I would have a preference for `t.$destroy` (or to a lesser extent `t.destroy$`).

> Or maybe the reverse?

One one side it would be more logical to do,
 - all class methods without $
 - PyProxy methods with $

however that would break backward compatibility and result in code with lots of $ which can be confusing visually.

So maybe let's go with, current methods as is + `$` methods to access shadowed class methods, as you proposed.
 
From the point of view of the type definitions, the Typescript type checker will have a very hard time if we use the Python object when there is a name collision. For this reason, I think the best bet is for e.g., `destroy` to always refer to the method that destroys the object and `$destroy` to refer to a Python field named `destroy`.

Otherwise, either the Typescript types will be wrong or unacceptably complicated. For example typescript will think that `destroy` is a method `(void) => void` and get upset when you try to pass it parameters even though the Python function being called may well expects parameters. You could make a bunch special of types "PyProxy but the destroy method has been shadowed" but it will be a nightmare to write and to use. On the other hand, if `destroy` always means the method that deletes the proxy, everything works great.
Incidentally, I don't know how to do this for `JsProxy` -- as far as I know there is no character that is valid in Python identifiers but not in Javascript ones. Python also doesn't have anything like `Symbol`s. This should probably be a separate issue though. 

Resolving patch:
diff --git a/conftest.py b/conftest.py
--- a/conftest.py
+++ b/conftest.py
@@ -109,7 +109,7 @@ def javascript_setup(self):
         self.run_js("Error.stackTraceLimit = Infinity;", pyodide_checks=False)
         self.run_js(
             """
-            window.assert = function assert(cb, message=""){
+            window.assert = function(cb, message=""){
                 if(message !== ""){
                     message = "\\n" + message;
                 }
@@ -117,37 +117,55 @@ def javascript_setup(self):
                     throw new Error(`Assertion failed: ${cb.toString().slice(6)}${message}`);
                 }
             };
-            window.assertThrows = function assert(cb, errname, pattern){
-                let pat_str = typeof pattern === "string" ? `"${pattern}"` : `${pattern}`;
-                let thiscallstr = `assertThrows(${cb.toString()}, "${errname}", ${pat_str})`;
+            window.assertAsync = async function(cb, message=""){
+                if(message !== ""){
+                    message = "\\n" + message;
+                }
+                if(await cb() !== true){
+                    throw new Error(`Assertion failed: ${cb.toString().slice(12)}${message}`);
+                }
+            };
+            function checkError(err, errname, pattern, pat_str, thiscallstr){
                 if(typeof pattern === "string"){
                     pattern = new RegExp(pattern);
                 }
-                let err = undefined;
-                try {
-                    cb();
-                } catch(e) {
-                    err = e;
-                }
-                console.log(err ? err.message : "no error");
                 if(!err){
-                    console.log("hi?");
                     throw new Error(`${thiscallstr} failed, no error thrown`);
                 }
                 if(err.constructor.name !== errname){
-                    console.log(err.toString());
                     throw new Error(
                         `${thiscallstr} failed, expected error ` +
                         `of type '${errname}' got type '${err.constructor.name}'`
                     );
                 }
                 if(!pattern.test(err.message)){
-                    console.log(err.toString());
                     throw new Error(
                         `${thiscallstr} failed, expected error ` +
                         `message to match pattern ${pat_str} got:\n${err.message}`
                     );
                 }
+            }
+            window.assertThrows = function(cb, errname, pattern){
+                let pat_str = typeof pattern === "string" ? `"${pattern}"` : `${pattern}`;
+                let thiscallstr = `assertThrows(${cb.toString()}, "${errname}", ${pat_str})`;
+                let err = undefined;
+                try {
+                    cb();
+                } catch(e) {
+                    err = e;
+                }
+                checkError(err, errname, pattern, pat_str, thiscallstr);
+            };
+            window.assertThrowsAsync = async function(cb, errname, pattern){
+                let pat_str = typeof pattern === "string" ? `"${pattern}"` : `${pattern}`;
+                let thiscallstr = `assertThrowsAsync(${cb.toString()}, "${errname}", ${pat_str})`;
+                let err = undefined;
+                try {
+                    await cb();
+                } catch(e) {
+                    err = e;
+                }
+                checkError(err, errname, pattern, pat_str, thiscallstr);
             };
             """,
             pyodide_checks=False,


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
@@ -392,8 +392,11 @@ def test_pyproxy_mixins2(selenium):
         assert(() => !("length" in get_method));
         assert(() => !("name" in get_method));
 
-        assert(() => pyodide.globals.get.type === "builtin_function_or_method");
-        assert(() => pyodide.globals.set.type === undefined);
+        let d = pyodide.runPython("{}");
+        assert(() => d.$get.type === "builtin_function_or_method");
+        assert(() => d.get.type === undefined);
+        assert(() => d.set.type === undefined);
+        d.destroy();
 
         let [Test, t] = pyodide.runPython(`
             class Test: pass
@@ -432,7 +435,8 @@ class Test:
                 length=7
             [Test, Test()]
         `);
-        assert(() => Test.prototype === "prototype");
+        assert(() => Test.$prototype === "prototype");
+        assert(() => Test.prototype === undefined);
         assert(() => Test.name==="me");
         assert(() => Test.length === 7);
 
@@ -451,8 +455,8 @@ def __len__(self):
         assert(() => !("length" in Test));
         assert(() => t.length === 9);
         t.length = 10;
-        assert(() => t.length === 10);
-        assert(() => t.__len__() === 9);
+        assert(() => t.length === 9);
+        assert(() => t.$length === 10);
 
         let l = pyodide.runPython(`
             l = [5, 6, 7] ; l
@@ -612,19 +616,6 @@ def test_pyproxy_copy(selenium):
 def test_errors(selenium):
     selenium.run_js(
         """
-        function expect_error(func){
-            let error = false;
-            try {
-                func();
-            } catch(e) {
-                if(e.name === "PythonError"){
-                    error = true;
-                }
-            }
-            if(!error){
-                throw new Error(`No PythonError ocurred: ${func.toString().slice(6)}`);
-            }
-        }
         let t = pyodide.runPython(`
             def te(self, *args, **kwargs):
                 raise Exception(repr(args))
@@ -644,19 +635,19 @@ class Temp:
                 __repr__ = te
             Temp()
         `);
-        expect_error(() => t.x);
-        expect_error(() => t.x = 2);
-        expect_error(() => delete t.x);
-        expect_error(() => Object.getOwnPropertyNames(t));
-        expect_error(() => t());
-        expect_error(() => t.get(1));
-        expect_error(() => t.set(1, 2));
-        expect_error(() => t.delete(1));
-        expect_error(() => t.has(1));
-        expect_error(() => t.length);
-        expect_error(() => t.then(()=>{}));
-        expect_error(() => t.toString());
-        expect_error(() => Array.from(t));
+        assertThrows(() => t.x, "PythonError", "");
+        assertThrows(() => t.x = 2, "PythonError", "");
+        assertThrows(() => delete t.x, "PythonError", "");
+        assertThrows(() => Object.getOwnPropertyNames(t), "PythonError", "");
+        assertThrows(() => t(), "PythonError", "");
+        assertThrows(() => t.get(1), "PythonError", "");
+        assertThrows(() => t.set(1, 2), "PythonError", "");
+        assertThrows(() => t.delete(1), "PythonError", "");
+        assertThrows(() => t.has(1), "PythonError", "");
+        assertThrows(() => t.length, "PythonError", "");
+        assertThrowsAsync(async () => await t, "PythonError", "");
+        assertThrows(() => t.toString(), "PythonError", "");
+        assertThrows(() => Array.from(t), "PythonError", "");
         """
     )
 
@@ -666,66 +657,69 @@ def test_fatal_error(selenium_standalone):
     selenium_standalone.run_js(
         """
         let fatal_error = false;
+        let old_fatal_error = pyodide._module.fatal_error;
         pyodide._module.fatal_error = (e) => {
             fatal_error = true;
             throw e;
         }
-        function expect_fatal(func){
-            fatal_error = false;
-            try {
-                func();
-            } catch(e) {
-                // pass
-            } finally {
-                if(!fatal_error){
-                    throw new Error(`No fatal error occured: ${func.toString().slice(6)}`);
+        try {
+            function expect_fatal(func){
+                fatal_error = false;
+                try {
+                    func();
+                } catch(e) {
+                    // pass
+                } finally {
+                    if(!fatal_error){
+                        throw new Error(`No fatal error occured: ${func.toString().slice(6)}`);
+                    }
                 }
             }
+            let t = pyodide.runPython(`
+                from _pyodide_core import trigger_fatal_error
+                def tfe(*args, **kwargs):
+                    trigger_fatal_error()
+                class Temp:
+                    __getattr__ = tfe
+                    __setattr__ = tfe
+                    __delattr__ = tfe
+                    __dir__ = tfe
+                    __call__ = tfe
+                    __getitem__ = tfe
+                    __setitem__ = tfe
+                    __delitem__ = tfe
+                    __iter__ = tfe
+                    __len__ = tfe
+                    __contains__ = tfe
+                    __await__ = tfe
+                    __repr__ = tfe
+                    __del__ = tfe
+                Temp()
+            `);
+            expect_fatal(() => "x" in t);
+            expect_fatal(() => t.x);
+            expect_fatal(() => t.x = 2);
+            expect_fatal(() => delete t.x);
+            expect_fatal(() => Object.getOwnPropertyNames(t));
+            expect_fatal(() => t());
+            expect_fatal(() => t.get(1));
+            expect_fatal(() => t.set(1, 2));
+            expect_fatal(() => t.delete(1));
+            expect_fatal(() => t.has(1));
+            expect_fatal(() => t.length);
+            expect_fatal(() => t.toString());
+            expect_fatal(() => Array.from(t));
+            t.destroy();
+            a = pyodide.runPython(`
+                from array import array
+                array("I", [1,2,3,4])
+            `);
+            b = a.getBuffer();
+            b._view_ptr = 1e10;
+            expect_fatal(() => b.release());
+        } finally {
+            pyodide._module.fatal_error = old_fatal_error;
         }
-        let t = pyodide.runPython(`
-            from _pyodide_core import trigger_fatal_error
-            def tfe(*args, **kwargs):
-                trigger_fatal_error()
-            class Temp:
-                __getattr__ = tfe
-                __setattr__ = tfe
-                __delattr__ = tfe
-                __dir__ = tfe
-                __call__ = tfe
-                __getitem__ = tfe
-                __setitem__ = tfe
-                __delitem__ = tfe
-                __iter__ = tfe
-                __len__ = tfe
-                __contains__ = tfe
-                __await__ = tfe
-                __repr__ = tfe
-                __del__ = tfe
-            Temp()
-        `);
-        expect_fatal(() => "x" in t);
-        expect_fatal(() => t.x);
-        expect_fatal(() => t.x = 2);
-        expect_fatal(() => delete t.x);
-        expect_fatal(() => Object.getOwnPropertyNames(t));
-        expect_fatal(() => t());
-        expect_fatal(() => t.get(1));
-        expect_fatal(() => t.set(1, 2));
-        expect_fatal(() => t.delete(1));
-        expect_fatal(() => t.has(1));
-        expect_fatal(() => t.length);
-        expect_fatal(() => t.then(()=>{}));
-        expect_fatal(() => t.toString());
-        expect_fatal(() => Array.from(t));
-        expect_fatal(() => t.destroy());
-        expect_fatal(() => t.destroy());
-        a = pyodide.runPython(`
-            from array import array
-            array("I", [1,2,3,4])
-        `);
-        b = a.getBuffer();
-        b._view_ptr = 1e10;
-        expect_fatal(() => b.release());
         """
     )
 
@@ -772,3 +766,23 @@ def assert_call(s, val):
     msg = r"TypeError: f\(\) got multiple values for argument 'x'"
     with pytest.raises(selenium.JavascriptException, match=msg):
         selenium.run_js("f.callKwargs(76, {x : 6})")
+
+
+def test_pyproxy_name_clash(selenium):
+    selenium.run_js(
+        """
+        let d = pyodide.runPython("{'a' : 2}");
+        assert(() => d.get('a') === 2);
+        assert(() => d.$get('b', 3) === 3);
+
+        let t = pyodide.runPython(`
+            class Test:
+                def destroy(self):
+                    return 7
+            Test()
+        `);
+        assert(() => t.$destroy() === 7);
+        t.destroy();
+        assertThrows(() => t.$destroy, "Error", "Object has already been destroyed");
+        """
+    )