Repository: pyodide/pyodide

Issue:
RunPythonAsync catch not working "Uncaught (in promise) Error"
Hi, I'm currently trying the Pyodide library inside a WebWorker, in a React project.

So my worker, will run the python code (which the user enter) and show the output even if there's an error.
Here is my code : 
```js
// myWorker.js
const data = {
  python: 'impt numpy as np'
}
languagePluginLoader.then(() => {
    pyodide
      .runPythonAsync(data.python, () => {})
      .then(results => {
        console.log(results);
        self.postMessage({ output: results });
      })
      .catch(err => {
        console.log(err);
        self.postMessage({ error: err.message });
    });
});
```

The problem here is the catch of `runPythonAsync(...).catch(err => {})` won't catch this kind of error thrown by `pyodide.asm.js` file, we'll only see it in the console..

![image](https://user-images.githubusercontent.com/18289388/75581248-21809f00-5a37-11ea-8bd8-88e8fbfc7baa.png)
```
Uncaught (in promise) Error: Traceback (most recent call last):
  File "/lib/python3.7/site-packages/pyodide.py", line 58, in find_imports
    mod = ast.parse(code)
  File "/lib/python3.7/ast.py", line 35, in parse
    return compile(source, filename, mode, PyCF_ONLY_AST)
  File "<unknown>", line 1
    impt numpy as np
             ^
SyntaxError: invalid syntax

    at _hiwire_throw_error (http://localhost:3000/static/modules/pyodide.asm.js:8:38727)
    at wasm-function[358]:0xfc6c0
    at wasm-function[386]:0xfe474
    at Object.Module.__findImports (http://localhost:3000/static/modules/pyodide.asm.js:8:1989949)
    at Object.Module.runPythonAsync (http://localhost:3000/static/modules/pyodide.asm.js:8:43347)
    at http://localhost:3000/443e11adcc88e32bff24.worker.js:106:8
```
So how I can handle this type of Python error ? Cause right now the catch function able me to catch error when something goes wrong in the `then(()=> {})` function.. 
Thanks.



Discussion:
Wrap the call in a try...catch block, and await the result inside the block

`try {await pyodide.runPythonAsync('imprt numpy')} catch (e) {console.log(e)}`
I manage to make it work without async/await method. 
```js
/* eslint-disable */
import languagePluginLoader from "./pyodide.js";

self.addEventListener("message", e => {
  languagePluginLoader.then(() => {
  const data = {
    python: 'impt numpy as np'
  }
   pyodide
      .runPythonAsync(data.python, () => {})
      .then(results => {
        self.postMessage({
          output: results,
          target: data.uniqueUrlWithId
        });
      })
      .catch(err => { // This catch won't be triggered, --this is the first catch--
        console.log(err);
      });
  }) // end of languagePluginLoader.then()
  .catch((error) => { // This one will be trigger but why ?
    console.log(error); 
  });
});
```

 Does the first catch will only be triggered if there's an error in the then function of `runPythonAsync()` ?
Hi, i face the same issue, use follow code.

```ts

            try {
                this.pyodide.runPythonAsync(
                    data.python,
                    (messageProgress: any) => {
                        console.error('messageProgress', messageProgress);   //  <====== this not be call, dont known why
                    },
                    (error: any) => {
                        // TODO cannot catch the error, it a bug
                        console.error('runPythonAsync error', error);
                        workerSelf.postMessage({error: error} as ResultType);   //  <====== this not be call, dont known why
                    },
                )
                    .then((results: any) => {
                        const stdout = this.pyodide.runPython('sys.stdout.getvalue()');
                        workerSelf.postMessage({
                            results: results,
                            print: {msg: stdout},
                            total: true,
                        } as ResultType);
                    }).catch((err: any) => {
                    // if you prefer messages with the error
                    workerSelf.postMessage({error: err.message} as ResultType);
                    // if you prefer onerror events
                    // setTimeout(() => { throw err; });
                }).finally(() => {
                    workerSelf.postMessage({allEnd: true} as ResultType);   //  <====== this not work
                });
            } catch (error) {
                console.error('runPythonAsync error', error);     //  <====== BUT this work
                workerSelf.postMessage({error: error} as ResultType);
            }

```

seems like the `pyodide.runPythonAsync` will throw a `Error` when it face `SyntaxError: invalid syntax` , but in js style , it must Reject the Promise, not throw a error.

i can sure it's a BUG .

---

BTW: the api document about `runPythonAsync` seems wrong.
https://pyodide.readthedocs.io/en/latest/js-api/pyodide_runPythonAsync.html#js-api-pyodide-runpythonasync
I just got bitten by a similar problem which I'm fixing in https://github.com/alexmojaki/futurecoder/pull/176

I don't think the original report applies any more since runPythonAsync no longer uses loadPackagesFromImports automatically, but I think it would still be best if `find_imports` caught SyntaxError and returned an empty list. I don't think users of these functions should have to check for valid syntax beforehand - it's not only inconvenient but easy to forget, assuming you even know it's necessary in the first place (I didn't). In fact I checked my original code with two maintainers here and it didn't seem to occur to them either.

Resolving patch:
diff --git a/src/py/_pyodide/_base.py b/src/py/_pyodide/_base.py
--- a/src/py/_pyodide/_base.py
+++ b/src/py/_pyodide/_base.py
@@ -515,7 +515,8 @@ def find_imports(source: str) -> List[str]:
     Returns
     -------
     ``List[str]``
-        A list of module names that are imported in ``source``.
+        A list of module names that are imported in ``source``. If ``source`` is not
+        syntactically correct Python code (after dedenting), returns an empty list.
 
     Examples
     --------
@@ -527,7 +528,10 @@ def find_imports(source: str) -> List[str]:
     # handle mis-indented input from multi-line strings
     source = dedent(source)
 
-    mod = ast.parse(source)
+    try:
+        mod = ast.parse(source)
+    except SyntaxError:
+        return []
     imports = set()
     for node in ast.walk(mod):
         if isinstance(node, ast.Import):


Test patch:
diff --git a/src/tests/test_pyodide.py b/src/tests/test_pyodide.py
--- a/src/tests/test_pyodide.py
+++ b/src/tests/test_pyodide.py
@@ -11,16 +11,26 @@
 def test_find_imports():
 
     res = find_imports(
-        dedent(
-            """
-           import numpy as np
-           from scipy import sparse
-           import matplotlib.pyplot as plt
-           """
-        )
+        """
+        import numpy as np
+        from scipy import sparse
+        import matplotlib.pyplot as plt
+        """
     )
     assert set(res) == {"numpy", "scipy", "matplotlib"}
 
+    # If there is a syntax error in the code, find_imports should return empty
+    # list.
+    res = find_imports(
+        """
+        import numpy as np
+        from scipy import sparse
+        import matplotlib.pyplot as plt
+        for x in [1,2,3]
+        """
+    )
+    assert res == []
+
 
 def test_code_runner():
     assert should_quiet("1+1;")